From 1868bab6900b7bdc0a2bda24b4c01f2c71b5e05d Mon Sep 17 00:00:00 2001 From: Garfie Date: Sat, 28 Mar 2026 12:49:37 -0500 Subject: [PATCH] Add linting and static code analysis --- .github/dependabot.yml | 10 + app/build.gradle.kts | 2 + .../com/librechat/android/MainActivity.kt | 5 +- .../android/navigation/DrawerContent.kt | 9 +- .../android/navigation/LibreChatNavHost.kt | 232 ++++---- .../android/navigation/NavHostViewModel.kt | 24 +- .../navigation/SettingsSidebarContent.kt | 2 +- .../android/navigation/TabletLayout.kt | 8 +- build-logic/convention/build.gradle.kts | 6 + .../AndroidApplicationConventionPlugin.kt | 5 + .../kotlin/AndroidComposeConventionPlugin.kt | 1 + .../kotlin/AndroidDetektConventionPlugin.kt | 28 + .../kotlin/AndroidLibraryConventionPlugin.kt | 7 + build.gradle.kts | 10 + config/detekt/detekt.yml | 113 ++++ .../android/core/common/extensions/FlowExt.kt | 1 - .../core/common/extensions/StringExt.kt | 7 +- .../core/common/result/SafeApiCallTest.kt | 4 +- .../core/data/datastore/TokenDataStore.kt | 4 +- .../android/core/data/mapper/MessageMapper.kt | 2 +- .../core/data/repository/AgentRepository.kt | 1 + .../data/repository/AgentRepositoryImpl.kt | 2 +- .../data/repository/ChatRepositoryImpl.kt | 2 - .../data/repository/ConfigRepositoryImpl.kt | 2 +- .../android/core/model/Conversation.kt | 1 - .../librechat/android/core/model/Endpoint.kt | 154 +++-- .../android/core/model/ParameterDefinition.kt | 27 +- .../android/core/model/mcp/McpServer.kt | 37 +- .../{CategoriesResponse.kt => Category.kt} | 0 .../android/core/network/api/AgentsApi.kt | 2 +- .../android/core/network/api/AuthApi.kt | 2 +- .../core/network/api/ConversationsApi.kt | 4 +- .../android/core/network/api/FilesApi.kt | 3 +- .../network/client/LibreChatHttpClient.kt | 1 - .../core/network/sse/SseEventMapper.kt | 23 +- .../android/core/network/sse/SseLineParser.kt | 4 +- .../core/ui/components/BannerDisplay.kt | 2 +- .../ui/components/DynamicParameterPanel.kt | 9 +- .../core/ui/components/DynamicSlider.kt | 4 +- .../core/ui/components/DynamicTagsInput.kt | 2 +- .../android/core/ui/components/ErrorBanner.kt | 2 +- .../core/ui/components/LibreChatTopBar.kt | 2 +- .../core/ui/components/LoadingIndicator.kt | 2 +- .../core/ui/components/ModelParameterSheet.kt | 13 +- .../ui/components/OtpVerificationDialog.kt | 7 +- .../ui/components/ScreenTransitionWrapper.kt | 3 +- .../librechat/android/core/ui/theme/Theme.kt | 7 +- .../agents/components/AgentActionsPanel.kt | 25 +- .../agents/components/AgentAdvancedPanel.kt | 4 +- .../agents/components/AgentAvatarPicker.kt | 2 +- .../components/AgentCapabilitiesSection.kt | 2 +- .../components/AgentCategorySelector.kt | 2 +- .../components/AgentCodeInterpreterSection.kt | 2 +- .../components/AgentFileSearchSection.kt | 2 +- .../agents/components/AgentHandoffConfig.kt | 2 +- .../components/AgentMcpToolsSelector.kt | 6 +- .../agents/components/AgentModelPicker.kt | 2 +- .../agents/components/AgentSharingSection.kt | 2 +- .../components/AgentSupportContactSection.kt | 2 +- .../agents/components/AgentVersionHistory.kt | 2 +- .../agents/components/ToolSelectDialog.kt | 2 +- .../agents/screen/AgentDetailScreen.kt | 23 +- .../agents/screen/AgentEditorScreen.kt | 6 +- .../agents/screen/AgentMarketplaceScreen.kt | 8 +- .../feature/agents/util/OpenApiSpecParser.kt | 1 + .../agents/viewmodel/AgentDetailViewModel.kt | 7 +- .../agents/viewmodel/AgentEditorViewModel.kt | 11 +- .../viewmodel/AgentMarketplaceViewModel.kt | 9 +- .../auth/screen/ForgotPasswordScreen.kt | 6 +- .../feature/auth/screen/LoginScreen.kt | 8 +- .../feature/auth/screen/RegisterScreen.kt | 6 +- .../auth/screen/ResetPasswordScreen.kt | 6 +- .../feature/auth/screen/ServerUrlScreen.kt | 6 +- .../feature/auth/screen/TermsScreen.kt | 6 +- .../feature/auth/screen/TwoFactorScreen.kt | 12 +- .../feature/auth/screen/VerifyEmailScreen.kt | 6 +- .../auth/viewmodel/ForgotPasswordViewModel.kt | 2 +- .../auth/viewmodel/RegisterViewModel.kt | 2 +- .../auth/viewmodel/ResetPasswordViewModel.kt | 2 +- .../auth/viewmodel/ServerUrlViewModel.kt | 2 +- .../feature/auth/viewmodel/TermsViewModel.kt | 2 +- .../auth/viewmodel/TwoFactorViewModel.kt | 2 +- .../auth/viewmodel/VerifyEmailViewModel.kt | 2 +- .../auth/viewmodel/RegisterViewModelTest.kt | 2 +- .../feature/chat/audio/VoiceRecorder.kt | 10 +- .../chat/components/AgentHandoffCard.kt | 5 +- .../chat/components/AttachmentChipsRow.kt | 8 +- .../feature/chat/components/AudioContent.kt | 2 - .../chat/components/AudioContentPlayer.kt | 10 +- .../feature/chat/components/ChatInput.kt | 34 +- .../chat/components/ChatInputToolbar.kt | 2 +- .../chat/components/ChatToolsBottomSheet.kt | 7 +- .../feature/chat/components/CitationChip.kt | 18 +- .../feature/chat/components/CodeBlock.kt | 9 +- .../chat/components/CodeExecutionCard.kt | 13 +- .../chat/components/ComparisonDualPane.kt | 4 +- .../chat/components/ComparisonTabBar.kt | 2 +- .../chat/components/ContentPartRenderer.kt | 112 ++-- .../chat/components/FeedbackCommentDialog.kt | 2 +- .../chat/components/ForkOptionsBottomSheet.kt | 4 +- .../chat/components/FullscreenImageViewer.kt | 7 +- .../feature/chat/components/ImageGenCard.kt | 4 +- .../chat/components/InConvoSearchBar.kt | 6 +- .../chat/components/InlineEditInput.kt | 3 +- .../feature/chat/components/LandingContent.kt | 4 +- .../feature/chat/components/LatexBlock.kt | 2 +- .../feature/chat/components/LogContentCard.kt | 2 +- .../chat/components/MarkdownContent.kt | 19 +- .../chat/components/McpResourceCarousel.kt | 2 +- .../chat/components/McpServerSelector.kt | 2 +- .../chat/components/MemoryArtifactCard.kt | 5 +- .../feature/chat/components/MermaidDiagram.kt | 5 +- .../feature/chat/components/MessageBubble.kt | 28 +- .../feature/chat/components/MessageFiles.kt | 6 +- .../feature/chat/components/MessageList.kt | 59 +- .../chat/components/ModelSelectorButton.kt | 2 +- .../chat/components/ModelSelectorSheet.kt | 9 +- .../feature/chat/components/PresetPicker.kt | 2 +- .../chat/components/SavePresetDialog.kt | 2 +- .../chat/components/SecondaryMessageList.kt | 4 +- .../chat/components/SiblingNavigator.kt | 2 +- .../chat/components/StreamingIndicator.kt | 2 +- .../chat/components/StreamingMessageBubble.kt | 4 +- .../chat/components/StreamingToolCallCard.kt | 11 +- .../feature/chat/components/VideoContent.kt | 2 +- .../chat/components/VideoContentPlayer.kt | 2 +- .../chat/components/WebSearchResultCard.kt | 3 +- .../components/artifact/ArtifactButton.kt | 6 +- .../chat/components/artifact/ArtifactPanel.kt | 8 +- .../components/artifact/ArtifactVersionNav.kt | 2 +- .../chat/prompts/PromptDetailScreen.kt | 6 +- .../chat/prompts/PromptEditorScreen.kt | 4 +- .../chat/prompts/PromptEditorViewModel.kt | 3 +- .../chat/prompts/PromptVariablesSection.kt | 2 +- .../chat/prompts/PromptVersionsSheet.kt | 2 +- .../chat/prompts/PromptsLibraryScreen.kt | 8 +- .../feature/chat/prompts/PromptsViewModel.kt | 4 + .../prompts/components/PromptFilterSheet.kt | 2 +- .../prompts/components/PromptPreviewPanel.kt | 2 +- .../prompts/components/PromptShareDialog.kt | 2 +- .../prompts/components/VariableInputDialog.kt | 2 +- .../android/feature/chat/screen/ChatScreen.kt | 45 +- .../util/{MessageTree.kt => MessageNode.kt} | 0 .../feature/chat/viewmodel/ChatViewModel.kt | 63 ++- .../delegate/FileAttachmentDelegate.kt | 12 +- .../delegate/ModelSelectionDelegate.kt | 3 - .../delegate/PresetPromptDelegate.kt | 7 +- .../delegate/TextToSpeechDelegate.kt | 9 +- feature/conversations/build.gradle.kts | 1 + .../components/ConversationActions.kt | 5 +- .../components/ConversationItem.kt | 156 +++--- .../components/ConversationSearchBar.kt | 2 +- .../conversations/components/TagFilterBar.kt | 2 +- .../conversations/components/TagPicker.kt | 4 +- .../export/ExportFormatPicker.kt | 4 +- .../screen/ArchivedConversationsScreen.kt | 13 +- .../screen/ConversationListScreen.kt | 29 +- .../ArchivedConversationsViewModel.kt | 3 + .../viewmodel/ConversationListViewModel.kt | 7 +- .../files/components/UploadProgressCard.kt | 2 +- .../android/feature/files/di/FilesModule.kt | 4 +- .../feature/files/screen/FilePreviewDialog.kt | 8 +- .../feature/files/screen/FileSortMenu.kt | 4 +- .../feature/files/screen/FilesScreen.kt | 111 ++-- .../feature/files/viewmodel/FilesViewModel.kt | 10 +- .../files/viewmodel/FilesViewModelTest.kt | 4 +- .../feature/settings/di/SettingsModule.kt | 4 +- .../settings/navigation/SettingsNavigation.kt | 2 +- .../settings/screen/AccountSettingsScreen.kt | 528 +++++++++--------- .../settings/screen/ApiKeyCreateDialog.kt | 4 +- .../feature/settings/screen/ApiKeysScreen.kt | 6 +- .../settings/screen/AvatarUploadDialog.kt | 4 +- .../feature/settings/screen/BalanceSection.kt | 90 +-- .../settings/screen/ChatSettingsScreen.kt | 448 +++++++-------- .../settings/screen/ChatSettingsSection.kt | 481 ++++++++-------- .../settings/screen/CommandsConfigScreen.kt | 2 +- .../settings/screen/DataSettingsScreen.kt | 327 +++++------ .../settings/screen/DataSettingsSection.kt | 182 +++--- .../settings/screen/ForkSettingsDialog.kt | 4 +- .../settings/screen/GeneralSettingsScreen.kt | 416 +++++++------- .../settings/screen/LanguageSelectorDialog.kt | 2 +- .../settings/screen/McpServerDialog.kt | 7 +- .../screen/McpServerStatusIndicator.kt | 7 +- .../settings/screen/McpServersScreen.kt | 7 +- .../settings/screen/McpSettingsSection.kt | 104 ++-- .../feature/settings/screen/McpToolList.kt | 2 +- .../feature/settings/screen/McpToolsSheet.kt | 5 +- .../feature/settings/screen/MemoriesScreen.kt | 7 +- .../screen/MemoriesSettingsSection.kt | 132 ++--- .../settings/screen/MemoryEditDialog.kt | 2 +- .../settings/screen/PersonalizationDialog.kt | 2 +- .../settings/screen/PresetManagerScreen.kt | 9 +- .../settings/screen/SecuritySection.kt | 110 ++-- .../feature/settings/screen/SettingsScreen.kt | 10 +- .../settings/screen/SharedLinksScreen.kt | 4 +- .../settings/screen/SpeechDetailDialogs.kt | 13 +- .../settings/screen/SpeechSettingsSection.kt | 266 ++++----- .../settings/screen/TabbedSettingsScreen.kt | 2 +- .../screen/sections/AccountSection.kt | 104 ++-- .../screen/sections/AppearanceSection.kt | 116 ++-- .../screen/sections/ChatPreferencesSection.kt | 138 ++--- .../screen/sections/DataManagementSection.kt | 84 +-- .../screen/sections/SecuritySection.kt | 1 - .../settings/screen/sections/SpeechSection.kt | 74 +-- .../settings/viewmodel/McpViewModel.kt | 6 +- .../settings/viewmodel/SettingsViewModel.kt | 52 +- .../delegate/DataManagementDelegate.kt | 1 - .../viewmodel/delegate/McpServerDelegate.kt | 6 +- .../delegate/MemoryManagementDelegate.kt | 4 +- .../delegate/SpeechSettingsDelegate.kt | 19 +- .../delegate/TwoFactorSecurityDelegate.kt | 1 - .../settings/src/main/res/values/strings.xml | 2 +- .../viewmodel/SettingsViewModelTest.kt | 4 +- gradle/libs.versions.toml | 13 +- lint.xml | 26 + 215 files changed, 3158 insertions(+), 2676 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 build-logic/convention/src/main/kotlin/AndroidDetektConventionPlugin.kt create mode 100644 config/detekt/detekt.yml rename core/model/src/main/kotlin/com/librechat/android/core/model/response/{CategoriesResponse.kt => Category.kt} (100%) rename feature/chat/src/main/kotlin/com/librechat/android/feature/chat/util/{MessageTree.kt => MessageNode.kt} (100%) create mode 100644 lint.xml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..b6498b9 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "gradle" + directory: "/" + schedule: + interval: "weekly" + +# Security scanning: Consider adding Snyk (https://snyk.io) for vulnerability +# detection. Snyk provides deeper analysis than Dependabot for transitive +# dependencies and can be integrated via GitHub Actions or the Snyk GitHub App. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 13f3eba..4a301b1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -39,6 +39,8 @@ dependencies { implementation(libs.coil.compose) implementation(libs.timber) + debugImplementation(libs.leakcanary) + androidTestImplementation(libs.compose.ui.test) debugImplementation(libs.compose.ui.test.manifest) } diff --git a/app/src/main/kotlin/com/librechat/android/MainActivity.kt b/app/src/main/kotlin/com/librechat/android/MainActivity.kt index 5f4a33e..4ca8cf6 100644 --- a/app/src/main/kotlin/com/librechat/android/MainActivity.kt +++ b/app/src/main/kotlin/com/librechat/android/MainActivity.kt @@ -23,14 +23,15 @@ import androidx.compose.material3.Text import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import androidx.compose.runtime.SideEffect -import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.core.view.WindowCompat +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.librechat.android.core.common.network.ConnectivityObserver import com.librechat.android.core.data.datastore.ThemeDataStore import com.librechat.android.core.data.datastore.ThemeMode @@ -40,7 +41,6 @@ import com.librechat.android.feature.chat.SharedContent import com.librechat.android.navigation.LibreChatNavHost import org.koin.android.ext.android.inject import timber.log.Timber -import androidx.compose.ui.res.stringResource class MainActivity : ComponentActivity() { @@ -153,6 +153,7 @@ class MainActivity : ComponentActivity() { private fun handleShareIntent(intent: Intent) { val sharedText = intent.getStringExtra(Intent.EXTRA_TEXT) + @Suppress("DEPRECATION") val sharedUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java) diff --git a/app/src/main/kotlin/com/librechat/android/navigation/DrawerContent.kt b/app/src/main/kotlin/com/librechat/android/navigation/DrawerContent.kt index aef6fb1..758bf92 100644 --- a/app/src/main/kotlin/com/librechat/android/navigation/DrawerContent.kt +++ b/app/src/main/kotlin/com/librechat/android/navigation/DrawerContent.kt @@ -48,16 +48,16 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.librechat.android.R import com.librechat.android.core.model.EModelEndpoint import com.librechat.android.core.ui.components.isMonochromeIcon import com.librechat.android.core.ui.components.toIconRes -import com.librechat.android.R -import androidx.compose.ui.res.stringResource /** * Lightweight snapshot of fields DrawerConversationItem actually renders. @@ -119,10 +119,10 @@ fun DrawerContent( onSettingsClick: () -> Unit, onAgentsClick: () -> Unit, onFilesClick: () -> Unit, + modifier: Modifier = Modifier, onToggleFavorite: (String) -> Unit = {}, onRefresh: () -> Unit = {}, onLoadMore: () -> Unit = {}, - modifier: Modifier = Modifier, ) { Column( modifier = modifier @@ -367,8 +367,8 @@ fun DrawerContent( private fun DrawerConversationItem( data: DrawerConversationDisplayData, onClick: () -> Unit, - onToggleFavorite: () -> Unit = {}, modifier: Modifier = Modifier, + onToggleFavorite: () -> Unit = {}, ) { val endpointIconRes = remember(data.endpoint) { data.endpoint?.toIconRes() @@ -517,4 +517,3 @@ private fun DrawerFooterItem( ) } } - diff --git a/app/src/main/kotlin/com/librechat/android/navigation/LibreChatNavHost.kt b/app/src/main/kotlin/com/librechat/android/navigation/LibreChatNavHost.kt index 9ddf6d0..1e0ca82 100644 --- a/app/src/main/kotlin/com/librechat/android/navigation/LibreChatNavHost.kt +++ b/app/src/main/kotlin/com/librechat/android/navigation/LibreChatNavHost.kt @@ -1,8 +1,6 @@ package com.librechat.android.navigation import android.net.Uri -import com.librechat.android.MainActivity -import timber.log.Timber import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.LinearEasing @@ -15,7 +13,6 @@ import androidx.compose.animation.slideOut import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.ui.unit.dp import androidx.compose.material3.AlertDialog import androidx.compose.material3.DrawerValue import androidx.compose.material3.MaterialTheme @@ -31,12 +28,16 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.IntOffset -import org.koin.compose.viewmodel.koinViewModel +import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.NavHost import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController +import com.librechat.android.MainActivity +import com.librechat.android.R +import com.librechat.android.core.ui.components.BannerDisplay import com.librechat.android.feature.agents.navigation.AGENT_EDITOR_CREATE_ROUTE import com.librechat.android.feature.agents.navigation.agentsGraph import com.librechat.android.feature.auth.navigation.AUTH_GRAPH_ROUTE @@ -50,7 +51,6 @@ import com.librechat.android.feature.conversations.navigation.conversationsGraph import com.librechat.android.feature.files.navigation.filesGraph import com.librechat.android.feature.settings.navigation.API_KEYS_ROUTE import com.librechat.android.feature.settings.navigation.PRESET_MANAGER_ROUTE -import com.librechat.android.core.ui.components.BannerDisplay import com.librechat.android.feature.settings.navigation.SETTINGS_ACCOUNT_ROUTE import com.librechat.android.feature.settings.navigation.SETTINGS_CHAT_ROUTE import com.librechat.android.feature.settings.navigation.SETTINGS_DATA_ROUTE @@ -59,8 +59,8 @@ import com.librechat.android.feature.settings.navigation.SETTINGS_TABBED_ROUTE import com.librechat.android.feature.settings.navigation.SHARED_LINKS_ROUTE import com.librechat.android.feature.settings.navigation.settingsGraph import kotlinx.coroutines.launch -import com.librechat.android.R -import androidx.compose.ui.res.stringResource +import org.koin.compose.viewmodel.koinViewModel +import timber.log.Timber /** Maps a [SettingsCategory] to its corresponding navigation route. */ fun SettingsCategory.toRoute(): String = when (this) { @@ -311,118 +311,118 @@ private fun PhoneLayout( navController = navController, startDestination = startDestination, modifier = Modifier.fillMaxSize(), - enterTransition = { - slideIntoContainer( - towards = AnimatedContentTransitionScope.SlideDirection.Start, - animationSpec = tween(300), + enterTransition = { + slideIntoContainer( + towards = AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(300), + ) + }, + exitTransition = { + fadeOut(animationSpec = tween(150)) + }, + popEnterTransition = { + fadeIn( + initialAlpha = 0.5f, + animationSpec = tween(300, easing = LinearEasing), + ) + scaleIn( + initialScale = 0.92f, + animationSpec = tween(300, easing = LinearEasing), + ) + }, + popExitTransition = { + slideOut( + targetOffset = { IntOffset((it.width * 0.15f).toInt(), 0) }, + animationSpec = tween(300, easing = FastOutSlowInEasing), + ) + scaleOut( + targetScale = 0.92f, + animationSpec = tween(300, easing = LinearEasing), + ) + }, + ) { + authGraph( + navController = navController, + onAuthComplete = { + navHostViewModel.onAuthComplete() + navController.navigate(CHAT_GRAPH_ROUTE) { + popUpTo(AUTH_GRAPH_ROUTE) { inclusive = true } + } + }, ) - }, - exitTransition = { - fadeOut(animationSpec = tween(150)) - }, - popEnterTransition = { - fadeIn( - initialAlpha = 0.5f, - animationSpec = tween(300, easing = LinearEasing), - ) + scaleIn( - initialScale = 0.92f, - animationSpec = tween(300, easing = LinearEasing), + chatGraph( + navController = navController, + onOpenDrawer = { + scope.launch { drawerState.open() } + }, ) - }, - popExitTransition = { - slideOut( - targetOffset = { IntOffset((it.width * 0.15f).toInt(), 0) }, - animationSpec = tween(300, easing = FastOutSlowInEasing), - ) + scaleOut( - targetScale = 0.92f, - animationSpec = tween(300, easing = LinearEasing), + conversationsGraph( + onConversationClick = { conversationId -> + navController.navigateToChat(conversationId) + }, + onNavigateToArchived = { + navController.navigate("conversations/archived") + }, + onNavigateBackFromArchived = { + navController.popBackStack() + }, ) - }, - ) { - authGraph( - navController = navController, - onAuthComplete = { - navHostViewModel.onAuthComplete() - navController.navigate(CHAT_GRAPH_ROUTE) { - popUpTo(AUTH_GRAPH_ROUTE) { inclusive = true } - } - }, - ) - chatGraph( - navController = navController, - onOpenDrawer = { - scope.launch { drawerState.open() } - }, - ) - conversationsGraph( - onConversationClick = { conversationId -> - navController.navigateToChat(conversationId) - }, - onNavigateToArchived = { - navController.navigate("conversations/archived") - }, - onNavigateBackFromArchived = { - navController.popBackStack() - }, - ) - agentsGraph( - onAgentClick = { agentId -> - navController.navigate("agents/$agentId") - }, - onBack = { navController.popBackStack() }, - onStartChat = { agentId -> - navController.navigate(NEW_CHAT_ROUTE) { - popUpTo(CHAT_GRAPH_ROUTE) { inclusive = false } - launchSingleTop = true - } - }, - onCreateAgent = { - navController.navigate(AGENT_EDITOR_CREATE_ROUTE) - }, - onEditAgent = { agentId -> - navController.navigate("agents/editor/$agentId") - }, - ) - filesGraph() - settingsGraph( - onLogout = { - navHostViewModel.logout() - navController.navigate(AUTH_GRAPH_ROUTE) { - popUpTo(0) { inclusive = true } - } - }, - onNavigateBack = { navController.popBackStack() }, - onNavigateToArchived = { - navController.navigate("conversations/archived") { - launchSingleTop = true - } - }, - onNavigateToSharedLinks = { - navController.navigate(SHARED_LINKS_ROUTE) { - launchSingleTop = true - } - }, - onNavigateBackFromSharedLinks = { - navController.popBackStack() - }, - onNavigateToPresets = { - navController.navigate(PRESET_MANAGER_ROUTE) { - launchSingleTop = true - } - }, - onNavigateBackFromPresets = { - navController.popBackStack() - }, - onNavigateToApiKeys = { - navController.navigate(API_KEYS_ROUTE) { - launchSingleTop = true - } - }, - onNavigateBackFromApiKeys = { - navController.popBackStack() - }, - ) - } + agentsGraph( + onAgentClick = { agentId -> + navController.navigate("agents/$agentId") + }, + onBack = { navController.popBackStack() }, + onStartChat = { agentId -> + navController.navigate(NEW_CHAT_ROUTE) { + popUpTo(CHAT_GRAPH_ROUTE) { inclusive = false } + launchSingleTop = true + } + }, + onCreateAgent = { + navController.navigate(AGENT_EDITOR_CREATE_ROUTE) + }, + onEditAgent = { agentId -> + navController.navigate("agents/editor/$agentId") + }, + ) + filesGraph() + settingsGraph( + onLogout = { + navHostViewModel.logout() + navController.navigate(AUTH_GRAPH_ROUTE) { + popUpTo(0) { inclusive = true } + } + }, + onNavigateBack = { navController.popBackStack() }, + onNavigateToArchived = { + navController.navigate("conversations/archived") { + launchSingleTop = true + } + }, + onNavigateToSharedLinks = { + navController.navigate(SHARED_LINKS_ROUTE) { + launchSingleTop = true + } + }, + onNavigateBackFromSharedLinks = { + navController.popBackStack() + }, + onNavigateToPresets = { + navController.navigate(PRESET_MANAGER_ROUTE) { + launchSingleTop = true + } + }, + onNavigateBackFromPresets = { + navController.popBackStack() + }, + onNavigateToApiKeys = { + navController.navigate(API_KEYS_ROUTE) { + launchSingleTop = true + } + }, + onNavigateBackFromApiKeys = { + navController.popBackStack() + }, + ) + } } } } diff --git a/app/src/main/kotlin/com/librechat/android/navigation/NavHostViewModel.kt b/app/src/main/kotlin/com/librechat/android/navigation/NavHostViewModel.kt index 982644f..c1c1b23 100644 --- a/app/src/main/kotlin/com/librechat/android/navigation/NavHostViewModel.kt +++ b/app/src/main/kotlin/com/librechat/android/navigation/NavHostViewModel.kt @@ -3,22 +3,16 @@ package com.librechat.android.navigation import androidx.compose.runtime.Immutable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.librechat.android.core.common.extensions.toInstantOrNull +import com.librechat.android.core.common.extensions.toRelativeDateGroup +import com.librechat.android.core.common.result.Result import com.librechat.android.core.data.repository.AuthRepository import com.librechat.android.core.data.repository.BannerRepository import com.librechat.android.core.data.repository.ConfigRepository import com.librechat.android.core.data.repository.ConversationRepository -import com.librechat.android.core.data.repository.VersionCheckResult -import com.librechat.android.core.common.result.Result -import com.librechat.android.core.common.extensions.toInstantOrNull -import com.librechat.android.core.common.extensions.toRelativeDateGroup import com.librechat.android.core.model.Banner import com.librechat.android.core.model.Conversation -import com.librechat.android.core.model.EModelEndpoint import com.librechat.android.core.network.client.TokenManager -import java.time.Instant -import java.time.ZoneId -import java.time.format.DateTimeFormatter -import java.time.temporal.ChronoUnit import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow @@ -32,6 +26,10 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import timber.log.Timber +import java.time.Instant +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.time.temporal.ChronoUnit /** * Combined UI state for the drawer sidebar. A single emission replaces @@ -155,7 +153,13 @@ class NavHostViewModel( * not the entire PhoneLayout/TabletLayout. */ val drawerUiState: StateFlow = combine( - combine(_groupedConversations, _activeConversationId, _favorites, _favoriteConversations, _searchQuery) { grouped, activeId, favIds, favConvos, query -> + combine( + _groupedConversations, + _activeConversationId, + _favorites, + _favoriteConversations, + _searchQuery, + ) { grouped, activeId, favIds, favConvos, query -> DrawerDataSnapshot(grouped, activeId, favIds, favConvos, query) }, combine(_isRefreshing, _isLoadingMore, _hasMore) { refreshing, loadingMore, hasMore -> diff --git a/app/src/main/kotlin/com/librechat/android/navigation/SettingsSidebarContent.kt b/app/src/main/kotlin/com/librechat/android/navigation/SettingsSidebarContent.kt index 8899b6d..5a5d184 100644 --- a/app/src/main/kotlin/com/librechat/android/navigation/SettingsSidebarContent.kt +++ b/app/src/main/kotlin/com/librechat/android/navigation/SettingsSidebarContent.kt @@ -27,11 +27,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import com.librechat.android.R -import androidx.compose.ui.res.stringResource /** * Settings sidebar content shown when the sidebar mode is [SidebarMode.Settings]. diff --git a/app/src/main/kotlin/com/librechat/android/navigation/TabletLayout.kt b/app/src/main/kotlin/com/librechat/android/navigation/TabletLayout.kt index 9ad5f86..f4fc91c 100644 --- a/app/src/main/kotlin/com/librechat/android/navigation/TabletLayout.kt +++ b/app/src/main/kotlin/com/librechat/android/navigation/TabletLayout.kt @@ -1,8 +1,6 @@ package com.librechat.android.navigation import android.net.Uri -import com.librechat.android.MainActivity -import timber.log.Timber import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.core.Animatable @@ -23,7 +21,6 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.ui.layout.Layout import androidx.compose.material3.MaterialTheme import androidx.compose.material3.VerticalDivider import androidx.compose.runtime.Composable @@ -35,12 +32,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.util.VelocityTracker +import androidx.compose.ui.layout.Layout import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost +import com.librechat.android.MainActivity import com.librechat.android.core.ui.components.BannerDisplay import com.librechat.android.feature.agents.navigation.AGENT_EDITOR_CREATE_ROUTE import com.librechat.android.feature.agents.navigation.agentsGraph @@ -59,6 +58,7 @@ import com.librechat.android.feature.settings.navigation.SETTINGS_TABBED_ROUTE import com.librechat.android.feature.settings.navigation.SHARED_LINKS_ROUTE import com.librechat.android.feature.settings.navigation.settingsGraph import kotlinx.coroutines.launch +import timber.log.Timber private val SidebarWidth = 320.dp @@ -73,8 +73,8 @@ fun TabletLayout( isInAuthFlow: Boolean, deepLinkUri: Uri?, onDeepLinkConsumed: () -> Unit, - shareNavigationTrigger: Int = 0, modifier: Modifier = Modifier, + shareNavigationTrigger: Int = 0, ) { // Banner state only -- drawer state is collected inside DrawerContent itself val banners by navHostViewModel.banners.collectAsStateWithLifecycle() diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index 60c985b..bf89854 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -8,6 +8,8 @@ dependencies { compileOnly(libs.compose.gradlePlugin) compileOnly(libs.ksp.gradlePlugin) compileOnly(libs.room.gradlePlugin) + compileOnly(libs.detekt.gradlePlugin) + compileOnly(libs.kover.gradlePlugin) } gradlePlugin { @@ -40,5 +42,9 @@ gradlePlugin { id = "librechat.kotlin.serialization" implementationClass = "KotlinSerializationConventionPlugin" } + register("androidDetekt") { + id = "librechat.android.detekt" + implementationClass = "AndroidDetektConventionPlugin" + } } } diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt index 09e884b..8731638 100644 --- a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt @@ -8,6 +8,8 @@ class AndroidApplicationConventionPlugin : Plugin { with(target) { pluginManager.apply("com.android.application") pluginManager.apply("org.jetbrains.kotlin.android") + pluginManager.apply("librechat.android.detekt") + pluginManager.apply("org.jetbrains.kotlinx.kover") extensions.configure { compileSdk = 35 @@ -23,6 +25,9 @@ class AndroidApplicationConventionPlugin : Plugin { targetCompatibility = org.gradle.api.JavaVersion.VERSION_17 isCoreLibraryDesugaringEnabled = true } + lint { + disable += "NullSafeMutableLiveData" + } buildTypes { release { isMinifyEnabled = true diff --git a/build-logic/convention/src/main/kotlin/AndroidComposeConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidComposeConventionPlugin.kt index dab5822..ab4fd79 100644 --- a/build-logic/convention/src/main/kotlin/AndroidComposeConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidComposeConventionPlugin.kt @@ -49,6 +49,7 @@ class AndroidComposeConventionPlugin : Plugin { add("debugImplementation", libs.findLibrary("compose-ui-tooling").get()) add("androidTestImplementation", platform(bom)) add("androidTestImplementation", libs.findLibrary("compose-ui-test").get()) + add("lintChecks", libs.findLibrary("compose-lint-checks").get()) } } } diff --git a/build-logic/convention/src/main/kotlin/AndroidDetektConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidDetektConventionPlugin.kt new file mode 100644 index 0000000..6a4f781 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/AndroidDetektConventionPlugin.kt @@ -0,0 +1,28 @@ +import io.gitlab.arturbosch.detekt.extensions.DetektExtension +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.dependencies + +class AndroidDetektConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + pluginManager.apply("io.gitlab.arturbosch.detekt") + + extensions.configure { + config.setFrom(rootProject.files("config/detekt/detekt.yml")) + buildUponDefaultConfig = true + allRules = false + parallel = true + } + + dependencies { + add("detektPlugins", libs.findLibrary("detekt-formatting").get()) + } + } + } +} + +private val Project.libs + get() = extensions.getByType(org.gradle.api.artifacts.VersionCatalogsExtension::class.java) + .named("libs") diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt index 32b2695..6af25b3 100644 --- a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt @@ -8,6 +8,8 @@ class AndroidLibraryConventionPlugin : Plugin { with(target) { pluginManager.apply("com.android.library") pluginManager.apply("org.jetbrains.kotlin.android") + pluginManager.apply("librechat.android.detekt") + pluginManager.apply("org.jetbrains.kotlinx.kover") extensions.configure { compileSdk = 35 @@ -20,6 +22,11 @@ class AndroidLibraryConventionPlugin : Plugin { targetCompatibility = org.gradle.api.JavaVersion.VERSION_17 isCoreLibraryDesugaringEnabled = true } + lint { + // Workaround: NonNullableMutableLiveDataDetector crashes with + // IncompatibleClassChangeError on AGP 8.7.x + Kotlin 2.1.x + disable += "NullSafeMutableLiveData" + } testOptions { unitTests.isReturnDefaultValues = true } diff --git a/build.gradle.kts b/build.gradle.kts index 31b1312..25f3f8b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,4 +6,14 @@ plugins { alias(libs.plugins.kotlin.serialization) apply false alias(libs.plugins.ksp) apply false alias(libs.plugins.room) apply false + alias(libs.plugins.detekt) apply false + alias(libs.plugins.kover) +} + +dependencies { + subprojects.forEach { subproject -> + if (subproject.buildFile.exists()) { + kover(subproject) + } + } } diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml new file mode 100644 index 0000000..5f200f6 --- /dev/null +++ b/config/detekt/detekt.yml @@ -0,0 +1,113 @@ +build: + maxIssues: 0 + +complexity: + LongMethod: + threshold: 300 + ignoreAnnotated: + - "Composable" + LongParameterList: + functionThreshold: 10 + constructorThreshold: 20 + ignoreDefaultParameters: true + ignoreAnnotated: + - "Composable" + TooManyFunctions: + thresholdInFiles: 30 + thresholdInClasses: 100 + thresholdInInterfaces: 25 + thresholdInObjects: 20 + thresholdInEnums: 10 + NestedBlockDepth: + threshold: 6 + CyclomaticComplexMethod: + threshold: 60 + ignoreAnnotated: + - "Composable" + ComplexCondition: + threshold: 10 + LargeClass: + threshold: 2500 + +style: + MagicNumber: + active: false + ReturnCount: + max: 12 + excludeReturnFromLambda: true + ignoreAnnotated: + - "Composable" + MaxLineLength: + maxLineLength: 140 + excludeCommentStatements: true + excludePackageStatements: true + excludeImportStatements: true + excludeRawStrings: true + ForbiddenComment: + active: false + UnusedPrivateMember: + active: true + allowedNames: "(_|ignored|expected|serialVersionUID)" + ignoreAnnotated: + - "Preview" + WildcardImport: + active: true + excludeImports: + - "kotlinx.coroutines.flow.*" + LoopWithTooManyJumpStatements: + maxJumpCount: 3 + ThrowsCount: + max: 4 + UseCheckOrError: + active: false + UseRequire: + active: false + +naming: + FunctionNaming: + ignoreAnnotated: + - "Composable" + TopLevelPropertyNaming: + constantPattern: "[A-Z][A-Za-z0-9_]*" + MatchingDeclarationName: + active: false + +exceptions: + TooGenericExceptionCaught: + active: true + exceptionNames: + - "RuntimeException" + - "Throwable" + allowedExceptionNameRegex: "_|(ignore|expected).*" + SwallowedException: + ignoredExceptionTypes: + - "InterruptedException" + - "MalformedURLException" + - "NumberFormatException" + - "ParseException" + allowedExceptionNameRegex: "_|(ignore|expected).*" + TooGenericExceptionThrown: + active: true + exceptionNames: + - "Error" + - "RuntimeException" + - "Throwable" + +coroutines: + active: true + +formatting: + MaximumLineLength: + active: false + TrailingCommaOnCallSite: + active: false + TrailingCommaOnDeclarationSite: + active: false + Wrapping: + active: false + ArgumentListWrapping: + active: false + ParameterListWrapping: + active: false + FunctionSignature: + active: false diff --git a/core/common/src/main/kotlin/com/librechat/android/core/common/extensions/FlowExt.kt b/core/common/src/main/kotlin/com/librechat/android/core/common/extensions/FlowExt.kt index c22480e..cf18735 100644 --- a/core/common/src/main/kotlin/com/librechat/android/core/common/extensions/FlowExt.kt +++ b/core/common/src/main/kotlin/com/librechat/android/core/common/extensions/FlowExt.kt @@ -2,7 +2,6 @@ package com.librechat.android.core.common.extensions import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.retryWhen import kotlin.math.pow diff --git a/core/common/src/main/kotlin/com/librechat/android/core/common/extensions/StringExt.kt b/core/common/src/main/kotlin/com/librechat/android/core/common/extensions/StringExt.kt index 9ce2459..362a277 100644 --- a/core/common/src/main/kotlin/com/librechat/android/core/common/extensions/StringExt.kt +++ b/core/common/src/main/kotlin/com/librechat/android/core/common/extensions/StringExt.kt @@ -12,5 +12,8 @@ fun String.ensureHttps(): String = when { } fun String.truncate(maxLength: Int, ellipsis: String = "..."): String = - if (length <= maxLength) this - else take(maxLength - ellipsis.length) + ellipsis + if (length <= maxLength) { + this + } else { + take(maxLength - ellipsis.length) + ellipsis + } diff --git a/core/common/src/test/kotlin/com/librechat/android/core/common/result/SafeApiCallTest.kt b/core/common/src/test/kotlin/com/librechat/android/core/common/result/SafeApiCallTest.kt index 280cd15..80459c5 100644 --- a/core/common/src/test/kotlin/com/librechat/android/core/common/result/SafeApiCallTest.kt +++ b/core/common/src/test/kotlin/com/librechat/android/core/common/result/SafeApiCallTest.kt @@ -15,11 +15,11 @@ class SafeApiCallTest { @Test fun `safeApiCall returns Error on exception`() = runTest { - val result = safeApiCall { throw RuntimeException("boom") } + val result = safeApiCall { throw IllegalStateException("boom") } assertThat(result).isInstanceOf(Result.Error::class.java) val error = result as Result.Error assertThat(error.message).isEqualTo("boom") - assertThat(error.exception).isInstanceOf(RuntimeException::class.java) + assertThat(error.exception).isInstanceOf(IllegalStateException::class.java) } @Test diff --git a/core/data/src/main/kotlin/com/librechat/android/core/data/datastore/TokenDataStore.kt b/core/data/src/main/kotlin/com/librechat/android/core/data/datastore/TokenDataStore.kt index 301d140..7912f20 100644 --- a/core/data/src/main/kotlin/com/librechat/android/core/data/datastore/TokenDataStore.kt +++ b/core/data/src/main/kotlin/com/librechat/android/core/data/datastore/TokenDataStore.kt @@ -15,14 +15,14 @@ import io.ktor.client.request.header import io.ktor.client.request.post import io.ktor.client.request.setBody import io.ktor.client.statement.HttpResponse -import java.io.IOException -import java.security.KeyStoreException import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import timber.log.Timber +import java.io.IOException +import java.security.KeyStoreException class TokenDataStore( context: Context, diff --git a/core/data/src/main/kotlin/com/librechat/android/core/data/mapper/MessageMapper.kt b/core/data/src/main/kotlin/com/librechat/android/core/data/mapper/MessageMapper.kt index 8fe8ba3..fb92075 100644 --- a/core/data/src/main/kotlin/com/librechat/android/core/data/mapper/MessageMapper.kt +++ b/core/data/src/main/kotlin/com/librechat/android/core/data/mapper/MessageMapper.kt @@ -1,9 +1,9 @@ package com.librechat.android.core.data.mapper import com.librechat.android.core.data.db.entity.MessageEntity +import com.librechat.android.core.model.Attachment import com.librechat.android.core.model.Feedback import com.librechat.android.core.model.FileReference -import com.librechat.android.core.model.Attachment import com.librechat.android.core.model.Message import com.librechat.android.core.model.MessageContentPart import kotlinx.serialization.builtins.ListSerializer diff --git a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/AgentRepository.kt b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/AgentRepository.kt index 8a138d6..7060d65 100644 --- a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/AgentRepository.kt +++ b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/AgentRepository.kt @@ -27,6 +27,7 @@ interface AgentRepository { category: String? = null, ): Result suspend fun getAgent(id: String): Result + /** * Fetches complete agent data for editing via the /expanded endpoint. * Unlike [getAgent] which returns limited view-only fields, this method diff --git a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/AgentRepositoryImpl.kt b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/AgentRepositoryImpl.kt index 14867d6..6fc4700 100644 --- a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/AgentRepositoryImpl.kt +++ b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/AgentRepositoryImpl.kt @@ -12,12 +12,12 @@ import com.librechat.android.core.model.ToolAuthStatus import com.librechat.android.core.model.ToolCallRecord import com.librechat.android.core.model.ToolCallResult import com.librechat.android.core.model.request.CreateActionRequest -import kotlinx.serialization.json.Json import com.librechat.android.core.model.request.CreateAgentRequest import com.librechat.android.core.model.request.RevertAgentRequest import com.librechat.android.core.model.request.ToolCallRequest import com.librechat.android.core.model.request.UpdateAgentRequest import com.librechat.android.core.network.api.AgentsApi +import kotlinx.serialization.json.Json class AgentRepositoryImpl( private val agentsApi: AgentsApi, diff --git a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/ChatRepositoryImpl.kt b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/ChatRepositoryImpl.kt index 5d104c3..5093be3 100644 --- a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/ChatRepositoryImpl.kt +++ b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/ChatRepositoryImpl.kt @@ -6,8 +6,6 @@ import com.librechat.android.core.common.result.safeApiCall import com.librechat.android.core.model.FileReference import com.librechat.android.core.model.StreamEvent import com.librechat.android.core.model.request.AddedConversation -import com.librechat.android.core.model.request.ChatAbortRequest -import com.librechat.android.core.model.request.ChatRequest import com.librechat.android.core.model.request.EphemeralAgent import com.librechat.android.core.model.response.ChatStatusResponse import com.librechat.android.core.network.api.ChatApi diff --git a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/ConfigRepositoryImpl.kt b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/ConfigRepositoryImpl.kt index dee252e..d1b2a17 100644 --- a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/ConfigRepositoryImpl.kt +++ b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/ConfigRepositoryImpl.kt @@ -1,13 +1,13 @@ package com.librechat.android.core.data.repository import com.librechat.android.core.common.BackendVersion +import com.librechat.android.core.common.result.ApiException import com.librechat.android.core.common.result.Result import com.librechat.android.core.common.result.safeApiCall import com.librechat.android.core.data.datastore.ConfigCacheDataStore import com.librechat.android.core.model.EndpointConfig import com.librechat.android.core.model.StartupConfig import com.librechat.android.core.network.api.ConfigApi -import com.librechat.android.core.common.result.ApiException import io.ktor.client.plugins.HttpRequestTimeoutException import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow diff --git a/core/model/src/main/kotlin/com/librechat/android/core/model/Conversation.kt b/core/model/src/main/kotlin/com/librechat/android/core/model/Conversation.kt index 57e48f0..bd3c420 100644 --- a/core/model/src/main/kotlin/com/librechat/android/core/model/Conversation.kt +++ b/core/model/src/main/kotlin/com/librechat/android/core/model/Conversation.kt @@ -3,7 +3,6 @@ package com.librechat.android.core.model import androidx.compose.runtime.Immutable import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonObject @Immutable @Serializable diff --git a/core/model/src/main/kotlin/com/librechat/android/core/model/Endpoint.kt b/core/model/src/main/kotlin/com/librechat/android/core/model/Endpoint.kt index 0938ce5..d5a5e25 100644 --- a/core/model/src/main/kotlin/com/librechat/android/core/model/Endpoint.kt +++ b/core/model/src/main/kotlin/com/librechat/android/core/model/Endpoint.kt @@ -5,64 +5,138 @@ import kotlinx.serialization.Serializable @Serializable enum class EModelEndpoint { - @SerialName("azureOpenAI") AZURE_OPENAI, - @SerialName("openAI") OPENAI, - @SerialName("google") GOOGLE, - @SerialName("anthropic") ANTHROPIC, - @SerialName("assistants") ASSISTANTS, - @SerialName("azureAssistants") AZURE_ASSISTANTS, - @SerialName("agents") AGENTS, - @SerialName("custom") CUSTOM, - @SerialName("bedrock") BEDROCK, + @SerialName("azureOpenAI") + AZURE_OPENAI, + + @SerialName("openAI") + OPENAI, + + @SerialName("google") + GOOGLE, + + @SerialName("anthropic") + ANTHROPIC, + + @SerialName("assistants") + ASSISTANTS, + + @SerialName("azureAssistants") + AZURE_ASSISTANTS, + + @SerialName("agents") + AGENTS, + + @SerialName("custom") + CUSTOM, + + @SerialName("bedrock") + BEDROCK, } @Serializable enum class ContentType { - @SerialName("text") TEXT, - @SerialName("think") THINK, - @SerialName("text_delta") TEXT_DELTA, - @SerialName("tool_call") TOOL_CALL, - @SerialName("image_file") IMAGE_FILE, - @SerialName("image_url") IMAGE_URL, - @SerialName("video_url") VIDEO_URL, - @SerialName("input_audio") INPUT_AUDIO, - @SerialName("agent_update") AGENT_UPDATE, - @SerialName("error") ERROR, + @SerialName("text") + TEXT, + + @SerialName("think") + THINK, + + @SerialName("text_delta") + TEXT_DELTA, + + @SerialName("tool_call") + TOOL_CALL, + + @SerialName("image_file") + IMAGE_FILE, + + @SerialName("image_url") + IMAGE_URL, + + @SerialName("video_url") + VIDEO_URL, + + @SerialName("input_audio") + INPUT_AUDIO, + + @SerialName("agent_update") + AGENT_UPDATE, + + @SerialName("error") + ERROR, } @Serializable enum class StepType { - @SerialName("tool_calls") TOOL_CALLS, - @SerialName("message_creation") MESSAGE_CREATION, + @SerialName("tool_calls") + TOOL_CALLS, + + @SerialName("message_creation") + MESSAGE_CREATION, } @Serializable enum class ToolCallType { - @SerialName("function") FUNCTION, - @SerialName("retrieval") RETRIEVAL, - @SerialName("file_search") FILE_SEARCH, - @SerialName("code_interpreter") CODE_INTERPRETER, - @SerialName("tool_call") TOOL_CALL, + @SerialName("function") + FUNCTION, + + @SerialName("retrieval") + RETRIEVAL, + + @SerialName("file_search") + FILE_SEARCH, + + @SerialName("code_interpreter") + CODE_INTERPRETER, + + @SerialName("tool_call") + TOOL_CALL, } @Serializable enum class FeedbackRating { - @SerialName("thumbsUp") THUMBS_UP, - @SerialName("thumbsDown") THUMBS_DOWN, + @SerialName("thumbsUp") + THUMBS_UP, + + @SerialName("thumbsDown") + THUMBS_DOWN, } @Serializable enum class Provider { - @SerialName("openAI") OPENAI, - @SerialName("anthropic") ANTHROPIC, - @SerialName("azureOpenAI") AZURE, - @SerialName("google") GOOGLE, - @SerialName("vertexai") VERTEXAI, - @SerialName("bedrock") BEDROCK, - @SerialName("mistralai") MISTRALAI, - @SerialName("mistral") MISTRAL, - @SerialName("deepseek") DEEPSEEK, - @SerialName("moonshot") MOONSHOT, - @SerialName("openrouter") OPENROUTER, - @SerialName("xai") XAI, + @SerialName("openAI") + OPENAI, + + @SerialName("anthropic") + ANTHROPIC, + + @SerialName("azureOpenAI") + AZURE, + + @SerialName("google") + GOOGLE, + + @SerialName("vertexai") + VERTEXAI, + + @SerialName("bedrock") + BEDROCK, + + @SerialName("mistralai") + MISTRALAI, + + @SerialName("mistral") + MISTRAL, + + @SerialName("deepseek") + DEEPSEEK, + + @SerialName("moonshot") + MOONSHOT, + + @SerialName("openrouter") + OPENROUTER, + + @SerialName("xai") + XAI, } diff --git a/core/model/src/main/kotlin/com/librechat/android/core/model/ParameterDefinition.kt b/core/model/src/main/kotlin/com/librechat/android/core/model/ParameterDefinition.kt index 36a3397..656f12b 100644 --- a/core/model/src/main/kotlin/com/librechat/android/core/model/ParameterDefinition.kt +++ b/core/model/src/main/kotlin/com/librechat/android/core/model/ParameterDefinition.kt @@ -22,11 +22,24 @@ data class ParameterDefinition( @Serializable enum class ParameterType { - @SerialName("slider") SLIDER, - @SerialName("dropdown") DROPDOWN, - @SerialName("checkbox") CHECKBOX, - @SerialName("text") TEXT, - @SerialName("switch") SWITCH, - @SerialName("textarea") TEXTAREA, - @SerialName("tags") TAGS, + @SerialName("slider") + SLIDER, + + @SerialName("dropdown") + DROPDOWN, + + @SerialName("checkbox") + CHECKBOX, + + @SerialName("text") + TEXT, + + @SerialName("switch") + SWITCH, + + @SerialName("textarea") + TEXTAREA, + + @SerialName("tags") + TAGS, } diff --git a/core/model/src/main/kotlin/com/librechat/android/core/model/mcp/McpServer.kt b/core/model/src/main/kotlin/com/librechat/android/core/model/mcp/McpServer.kt index df699d8..a593b9a 100644 --- a/core/model/src/main/kotlin/com/librechat/android/core/model/mcp/McpServer.kt +++ b/core/model/src/main/kotlin/com/librechat/android/core/model/mcp/McpServer.kt @@ -25,11 +25,20 @@ data class McpServer( @Serializable enum class McpServerType { - @SerialName("sse") SSE, - @SerialName("streamable-http") STREAMABLE_HTTP, - @SerialName("http") HTTP, - @SerialName("stdio") STDIO, - @SerialName("websocket") WEBSOCKET, + @SerialName("sse") + SSE, + + @SerialName("streamable-http") + STREAMABLE_HTTP, + + @SerialName("http") + HTTP, + + @SerialName("stdio") + STDIO, + + @SerialName("websocket") + WEBSOCKET, } /** Auth mode for the MCP server add/edit dialog. */ @@ -47,15 +56,23 @@ data class McpApiKeyConfig( @Serializable enum class McpApiKeySource { - @SerialName("admin") ADMIN, - @SerialName("user") USER, + @SerialName("admin") + ADMIN, + + @SerialName("user") + USER, } @Serializable enum class McpAuthorizationType { - @SerialName("bearer") BEARER, - @SerialName("basic") BASIC, - @SerialName("custom") CUSTOM, + @SerialName("bearer") + BEARER, + + @SerialName("basic") + BASIC, + + @SerialName("custom") + CUSTOM, } /** Maps to backend oauth object. Only the most common fields are exposed in the UI. */ diff --git a/core/model/src/main/kotlin/com/librechat/android/core/model/response/CategoriesResponse.kt b/core/model/src/main/kotlin/com/librechat/android/core/model/response/Category.kt similarity index 100% rename from core/model/src/main/kotlin/com/librechat/android/core/model/response/CategoriesResponse.kt rename to core/model/src/main/kotlin/com/librechat/android/core/model/response/Category.kt diff --git a/core/network/src/main/kotlin/com/librechat/android/core/network/api/AgentsApi.kt b/core/network/src/main/kotlin/com/librechat/android/core/network/api/AgentsApi.kt index 0c49263..29f0e18 100644 --- a/core/network/src/main/kotlin/com/librechat/android/core/network/api/AgentsApi.kt +++ b/core/network/src/main/kotlin/com/librechat/android/core/network/api/AgentsApi.kt @@ -16,7 +16,6 @@ import com.librechat.android.core.model.request.UpdateAgentRequest import com.librechat.android.core.model.response.AgentListResponse import io.ktor.client.HttpClient import io.ktor.client.call.body -import kotlinx.serialization.json.JsonArray import io.ktor.client.request.delete import io.ktor.client.request.forms.formData import io.ktor.client.request.forms.submitFormWithBinaryData @@ -28,6 +27,7 @@ import io.ktor.client.request.setBody import io.ktor.http.Headers import io.ktor.http.HttpHeaders import io.ktor.http.path +import kotlinx.serialization.json.JsonArray import timber.log.Timber class AgentsApi constructor( diff --git a/core/network/src/main/kotlin/com/librechat/android/core/network/api/AuthApi.kt b/core/network/src/main/kotlin/com/librechat/android/core/network/api/AuthApi.kt index 1fdd7cb..28835a1 100644 --- a/core/network/src/main/kotlin/com/librechat/android/core/network/api/AuthApi.kt +++ b/core/network/src/main/kotlin/com/librechat/android/core/network/api/AuthApi.kt @@ -1,11 +1,11 @@ package com.librechat.android.core.network.api import com.librechat.android.core.model.request.LoginRequest +import com.librechat.android.core.model.request.OtpVerificationRequest import com.librechat.android.core.model.request.PasswordResetRequest import com.librechat.android.core.model.request.RegisterRequest import com.librechat.android.core.model.request.ResetPasswordRequest import com.librechat.android.core.model.request.TwoFactorDisableRequest -import com.librechat.android.core.model.request.OtpVerificationRequest import com.librechat.android.core.model.request.TwoFactorVerifyRequest import com.librechat.android.core.model.request.TwoFactorVerifyTempRequest import com.librechat.android.core.model.response.LoginResponse diff --git a/core/network/src/main/kotlin/com/librechat/android/core/network/api/ConversationsApi.kt b/core/network/src/main/kotlin/com/librechat/android/core/network/api/ConversationsApi.kt index 1cb16bf..ceb3d15 100644 --- a/core/network/src/main/kotlin/com/librechat/android/core/network/api/ConversationsApi.kt +++ b/core/network/src/main/kotlin/com/librechat/android/core/network/api/ConversationsApi.kt @@ -1,10 +1,10 @@ package com.librechat.android.core.network.api import com.librechat.android.core.model.Conversation -import com.librechat.android.core.model.request.ArchiveConversationRequest import com.librechat.android.core.model.request.ArchiveConversationArg -import com.librechat.android.core.model.request.ConvoDeleteBody +import com.librechat.android.core.model.request.ArchiveConversationRequest import com.librechat.android.core.model.request.ConvoDeleteArg +import com.librechat.android.core.model.request.ConvoDeleteBody import com.librechat.android.core.model.request.ConvoUpdateArg import com.librechat.android.core.model.request.ConvoUpdateBody import com.librechat.android.core.model.request.DuplicateConversationRequest diff --git a/core/network/src/main/kotlin/com/librechat/android/core/network/api/FilesApi.kt b/core/network/src/main/kotlin/com/librechat/android/core/network/api/FilesApi.kt index 927e493..f6c8a33 100644 --- a/core/network/src/main/kotlin/com/librechat/android/core/network/api/FilesApi.kt +++ b/core/network/src/main/kotlin/com/librechat/android/core/network/api/FilesApi.kt @@ -54,7 +54,8 @@ class FilesApi constructor( height: Int? = null, ): FileObject { Timber.d( - "uploadFile: filename=%s, type=%s, size=%d bytes, fileId=%s, endpoint=%s, model=%s, agentId=%s, messageFile=%s, width=%s, height=%s", + "uploadFile: filename=%s, type=%s, size=%d bytes, fileId=%s, " + + "endpoint=%s, model=%s, agentId=%s, messageFile=%s, width=%s, height=%s", filename, type, bytes.size, fileId, endpoint, model, agentId, messageFile, width, height, ) diff --git a/core/network/src/main/kotlin/com/librechat/android/core/network/client/LibreChatHttpClient.kt b/core/network/src/main/kotlin/com/librechat/android/core/network/client/LibreChatHttpClient.kt index 623bf4b..2260103 100644 --- a/core/network/src/main/kotlin/com/librechat/android/core/network/client/LibreChatHttpClient.kt +++ b/core/network/src/main/kotlin/com/librechat/android/core/network/client/LibreChatHttpClient.kt @@ -35,7 +35,6 @@ object LibreChatHttpClient { cacheDir: File? = null, debug: Boolean = false, ): HttpClient = HttpClient(OkHttp) { - engine { if (cacheDir != null) { config { diff --git a/core/network/src/main/kotlin/com/librechat/android/core/network/sse/SseEventMapper.kt b/core/network/src/main/kotlin/com/librechat/android/core/network/sse/SseEventMapper.kt index fa285bc..321adaf 100644 --- a/core/network/src/main/kotlin/com/librechat/android/core/network/sse/SseEventMapper.kt +++ b/core/network/src/main/kotlin/com/librechat/android/core/network/sse/SseEventMapper.kt @@ -120,8 +120,7 @@ class SseEventMapper(private val json: Json) { val parseErrors = mutableListOf() val conversation = root["conversation"]?.let { - try { json.decodeFromJsonElement(Conversation.serializer(), it) } - catch (e: Exception) { + try { json.decodeFromJsonElement(Conversation.serializer(), it) } catch (e: Exception) { val msg = "Failed to parse final conversation: ${e.message}" Timber.w(e, msg) parseErrors.add(msg) @@ -129,8 +128,7 @@ class SseEventMapper(private val json: Json) { } } val requestMessage = root["requestMessage"]?.let { - try { json.decodeFromJsonElement(Message.serializer(), it) } - catch (e: Exception) { + try { json.decodeFromJsonElement(Message.serializer(), it) } catch (e: Exception) { val msg = "Failed to parse final requestMessage: ${e.message}" Timber.w(e, msg) parseErrors.add(msg) @@ -138,8 +136,7 @@ class SseEventMapper(private val json: Json) { } } val responseMessage = root["responseMessage"]?.let { - try { json.decodeFromJsonElement(Message.serializer(), it) } - catch (e: Exception) { + try { json.decodeFromJsonElement(Message.serializer(), it) } catch (e: Exception) { val msg = "Failed to parse final responseMessage: ${e.message}" Timber.w(e, msg) parseErrors.add(msg) @@ -148,8 +145,7 @@ class SseEventMapper(private val json: Json) { } // Legacy: some events use "message" instead of "responseMessage" val legacyMessage = root["message"]?.let { - try { json.decodeFromJsonElement(Message.serializer(), it) } - catch (e: Exception) { + try { json.decodeFromJsonElement(Message.serializer(), it) } catch (e: Exception) { val msg = "Failed to parse final legacy message: ${e.message}" Timber.w(e, msg) parseErrors.add(msg) @@ -158,8 +154,8 @@ class SseEventMapper(private val json: Json) { } // If ALL critical fields failed to parse, emit an Error instead - val allFieldsNull = conversation == null && requestMessage == null - && responseMessage == null && legacyMessage == null + val allFieldsNull = conversation == null && requestMessage == null && + responseMessage == null && legacyMessage == null if (allFieldsNull && parseErrors.isNotEmpty()) { Timber.e("Final event: all fields failed to parse -- %s", parseErrors) return StreamEvent.Error( @@ -256,7 +252,7 @@ class SseEventMapper(private val json: Json) { "on_message_delta" -> mapMessageDelta(data, agentId, groupId) "on_reasoning_delta" -> mapReasoningDelta(data, agentId, groupId) "on_run_step" -> mapRunStep(data, agentId, groupId) - "on_run_step_delta" -> mapRunStepDelta(data) + "on_run_step_delta" -> null // Tool call argument streaming - not currently tracked "on_run_step_completed" -> mapRunStepCompleted(data, agentId, groupId) "on_chat_model_end" -> null "on_agent_update" -> null @@ -347,11 +343,6 @@ class SseEventMapper(private val json: Json) { ) } - private fun mapRunStepDelta(data: JsonObject): StreamEvent? { - // Tool call argument streaming - not currently tracked - return null - } - private fun mapRunStepCompleted( data: JsonObject, agentId: String?, diff --git a/core/network/src/main/kotlin/com/librechat/android/core/network/sse/SseLineParser.kt b/core/network/src/main/kotlin/com/librechat/android/core/network/sse/SseLineParser.kt index 4c79421..33c0a0c 100644 --- a/core/network/src/main/kotlin/com/librechat/android/core/network/sse/SseLineParser.kt +++ b/core/network/src/main/kotlin/com/librechat/android/core/network/sse/SseLineParser.kt @@ -23,8 +23,8 @@ class SseLineParser( channel.readUTF8Line() } } catch (e: kotlinx.coroutines.TimeoutCancellationException) { - Timber.w("SSE stream stalled: no data for ${lineReadTimeoutMs / 1000}s") - throw IOException("Stream stalled: no data received for ${lineReadTimeoutMs / 1000}s") + Timber.w(e, "SSE stream stalled: no data for ${lineReadTimeoutMs / 1000}s") + throw IOException("Stream stalled: no data received for ${lineReadTimeoutMs / 1000}s", e) } ?: break when { diff --git a/core/ui/src/main/kotlin/com/librechat/android/core/ui/components/BannerDisplay.kt b/core/ui/src/main/kotlin/com/librechat/android/core/ui/components/BannerDisplay.kt index 5b8348d..f996b98 100644 --- a/core/ui/src/main/kotlin/com/librechat/android/core/ui/components/BannerDisplay.kt +++ b/core/ui/src/main/kotlin/com/librechat/android/core/ui/components/BannerDisplay.kt @@ -26,13 +26,13 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.LiveRegionMode import androidx.compose.ui.semantics.liveRegion import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import com.librechat.android.core.model.Banner import com.librechat.android.core.ui.R -import androidx.compose.ui.res.stringResource /** * Renders a vertical stack of dismissible server banners. diff --git a/core/ui/src/main/kotlin/com/librechat/android/core/ui/components/DynamicParameterPanel.kt b/core/ui/src/main/kotlin/com/librechat/android/core/ui/components/DynamicParameterPanel.kt index 2dba387..23571f7 100644 --- a/core/ui/src/main/kotlin/com/librechat/android/core/ui/components/DynamicParameterPanel.kt +++ b/core/ui/src/main/kotlin/com/librechat/android/core/ui/components/DynamicParameterPanel.kt @@ -11,6 +11,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview @@ -18,7 +19,6 @@ import androidx.compose.ui.unit.dp import com.librechat.android.core.model.ParameterDefinition import com.librechat.android.core.model.ParameterType import com.librechat.android.core.ui.R -import androidx.compose.ui.res.stringResource /** Dispatches a list of ParameterDefinitions to typed controls (slider, dropdown, checkbox, input, textarea) by ParameterType. */ @Composable @@ -113,8 +113,11 @@ fun DynamicParameterPanel( ParameterType.TAGS -> { DynamicTagsInput( label = definition.label, - tags = if (currentValue.isBlank()) emptyList() - else currentValue.split("\n").filter { it.isNotBlank() }, + tags = if (currentValue.isBlank()) { + emptyList() + } else { + currentValue.split("\n").filter { it.isNotBlank() } + }, onTagsChanged = { tags -> onValueChanged(definition.key, tags.joinToString("\n")) }, diff --git a/core/ui/src/main/kotlin/com/librechat/android/core/ui/components/DynamicSlider.kt b/core/ui/src/main/kotlin/com/librechat/android/core/ui/components/DynamicSlider.kt index 02d5f4a..7e08840 100644 --- a/core/ui/src/main/kotlin/com/librechat/android/core/ui/components/DynamicSlider.kt +++ b/core/ui/src/main/kotlin/com/librechat/android/core/ui/components/DynamicSlider.kt @@ -14,12 +14,12 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview -import kotlin.math.roundToInt import com.librechat.android.core.ui.R -import androidx.compose.ui.res.stringResource +import kotlin.math.roundToInt /** Labeled slider that snaps to step increments, with stepCount derived from (max - min) / step. */ @Composable diff --git a/core/ui/src/main/kotlin/com/librechat/android/core/ui/components/DynamicTagsInput.kt b/core/ui/src/main/kotlin/com/librechat/android/core/ui/components/DynamicTagsInput.kt index 6f4f5d7..7b4f56f 100644 --- a/core/ui/src/main/kotlin/com/librechat/android/core/ui/components/DynamicTagsInput.kt +++ b/core/ui/src/main/kotlin/com/librechat/android/core/ui/components/DynamicTagsInput.kt @@ -24,11 +24,11 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.librechat.android.core.ui.R -import androidx.compose.ui.res.stringResource /** Tag input that renders chips and allows adding/removing text tags up to a max count. */ @OptIn(ExperimentalLayoutApi::class) diff --git a/core/ui/src/main/kotlin/com/librechat/android/core/ui/components/ErrorBanner.kt b/core/ui/src/main/kotlin/com/librechat/android/core/ui/components/ErrorBanner.kt index 506198f..809696e 100644 --- a/core/ui/src/main/kotlin/com/librechat/android/core/ui/components/ErrorBanner.kt +++ b/core/ui/src/main/kotlin/com/librechat/android/core/ui/components/ErrorBanner.kt @@ -16,12 +16,12 @@ import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.LiveRegionMode import androidx.compose.ui.semantics.liveRegion import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import com.librechat.android.core.ui.R -import androidx.compose.ui.res.stringResource @Composable fun ErrorBanner( diff --git a/core/ui/src/main/kotlin/com/librechat/android/core/ui/components/LibreChatTopBar.kt b/core/ui/src/main/kotlin/com/librechat/android/core/ui/components/LibreChatTopBar.kt index ea60e5d..44929b8 100644 --- a/core/ui/src/main/kotlin/com/librechat/android/core/ui/components/LibreChatTopBar.kt +++ b/core/ui/src/main/kotlin/com/librechat/android/core/ui/components/LibreChatTopBar.kt @@ -12,8 +12,8 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.librechat.android.core.ui.R import androidx.compose.ui.res.stringResource +import com.librechat.android.core.ui.R @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/core/ui/src/main/kotlin/com/librechat/android/core/ui/components/LoadingIndicator.kt b/core/ui/src/main/kotlin/com/librechat/android/core/ui/components/LoadingIndicator.kt index c59b9d5..3499d87 100644 --- a/core/ui/src/main/kotlin/com/librechat/android/core/ui/components/LoadingIndicator.kt +++ b/core/ui/src/main/kotlin/com/librechat/android/core/ui/components/LoadingIndicator.kt @@ -7,10 +7,10 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import com.librechat.android.core.ui.R -import androidx.compose.ui.res.stringResource @Composable fun LoadingIndicator( diff --git a/core/ui/src/main/kotlin/com/librechat/android/core/ui/components/ModelParameterSheet.kt b/core/ui/src/main/kotlin/com/librechat/android/core/ui/components/ModelParameterSheet.kt index 99c412e..817f0d8 100644 --- a/core/ui/src/main/kotlin/com/librechat/android/core/ui/components/ModelParameterSheet.kt +++ b/core/ui/src/main/kotlin/com/librechat/android/core/ui/components/ModelParameterSheet.kt @@ -21,13 +21,13 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import com.librechat.android.core.model.ParameterDefinition import com.librechat.android.core.model.ParameterType import com.librechat.android.core.ui.R -import androidx.compose.ui.res.stringResource @Stable data class ModelParameters( @@ -174,7 +174,9 @@ fun ModelParameterContent( onValueChange = { newValue -> onParametersChanged(parameters.withUpdatedKey(definition.key, newValue)) }, - placeholder = definition.default?.ifEmpty { stringResource(R.string.default_placeholder) } ?: stringResource(R.string.default_placeholder), + placeholder = definition.default?.ifEmpty { + stringResource(R.string.default_placeholder) + } ?: stringResource(R.string.default_placeholder), description = definition.description, ) } @@ -243,8 +245,11 @@ fun ModelParameterContent( } ParameterType.TAGS -> { - val tags = if (currentValue.isBlank()) emptyList() - else currentValue.split("\n").filter { it.isNotBlank() } + val tags = if (currentValue.isBlank()) { + emptyList() + } else { + currentValue.split("\n").filter { it.isNotBlank() } + } DynamicTagsInput( label = definition.label, diff --git a/core/ui/src/main/kotlin/com/librechat/android/core/ui/components/OtpVerificationDialog.kt b/core/ui/src/main/kotlin/com/librechat/android/core/ui/components/OtpVerificationDialog.kt index c12ccb0..3c228e7 100644 --- a/core/ui/src/main/kotlin/com/librechat/android/core/ui/components/OtpVerificationDialog.kt +++ b/core/ui/src/main/kotlin/com/librechat/android/core/ui/components/OtpVerificationDialog.kt @@ -7,10 +7,9 @@ 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.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField @@ -57,10 +56,10 @@ import androidx.compose.ui.unit.dp @Composable fun OtpVerificationDialog( title: String, - description: String? = null, - isLoading: Boolean = false, onVerify: (token: String?, backupCode: String?) -> Unit, onDismiss: () -> Unit, + description: String? = null, + isLoading: Boolean = false, verifyLabel: String = "Verify", cancelLabel: String = "Cancel", backupCodeLabel: String = "Backup Code", diff --git a/core/ui/src/main/kotlin/com/librechat/android/core/ui/components/ScreenTransitionWrapper.kt b/core/ui/src/main/kotlin/com/librechat/android/core/ui/components/ScreenTransitionWrapper.kt index 52c8572..18cf50f 100644 --- a/core/ui/src/main/kotlin/com/librechat/android/core/ui/components/ScreenTransitionWrapper.kt +++ b/core/ui/src/main/kotlin/com/librechat/android/core/ui/components/ScreenTransitionWrapper.kt @@ -28,6 +28,7 @@ private const val FALLBACK_CORNER_RADIUS_DP = 24f @Composable fun ScreenTransitionWrapper( transition: Transition, + modifier: Modifier = Modifier, content: @Composable () -> Unit, ) { val screenCornerRadiusDp = rememberScreenCornerRadiusDp() @@ -43,7 +44,7 @@ fun ScreenTransitionWrapper( } } Box( - modifier = Modifier + modifier = modifier .fillMaxSize() .clip(RoundedCornerShape(cornerRadius.dp)), ) { diff --git a/core/ui/src/main/kotlin/com/librechat/android/core/ui/theme/Theme.kt b/core/ui/src/main/kotlin/com/librechat/android/core/ui/theme/Theme.kt index 2ed8066..7a7cb69 100644 --- a/core/ui/src/main/kotlin/com/librechat/android/core/ui/theme/Theme.kt +++ b/core/ui/src/main/kotlin/com/librechat/android/core/ui/theme/Theme.kt @@ -62,8 +62,11 @@ fun LibreChatTheme( ) { val colorScheme = when { dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - if (darkTheme) dynamicDarkColorScheme(LocalContext.current) - else dynamicLightColorScheme(LocalContext.current) + if (darkTheme) { + dynamicDarkColorScheme(LocalContext.current) + } else { + dynamicLightColorScheme(LocalContext.current) + } } darkTheme -> libreChatDarkColorScheme else -> libreChatLightColorScheme diff --git a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentActionsPanel.kt b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentActionsPanel.kt index ed31f09..39c7fea 100644 --- a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentActionsPanel.kt +++ b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentActionsPanel.kt @@ -45,6 +45,7 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.PasswordVisualTransformation @@ -56,11 +57,10 @@ import com.librechat.android.core.model.ActionAuth import com.librechat.android.core.model.ActionMetadata import com.librechat.android.core.model.request.FunctionTool import com.librechat.android.feature.agents.AgentActionDisplayData +import com.librechat.android.feature.agents.R import com.librechat.android.feature.agents.util.OpenApiSpecParser import com.librechat.android.feature.agents.util.ParsedFunctionInfo import kotlinx.coroutines.delay -import com.librechat.android.feature.agents.R -import androidx.compose.ui.res.stringResource private const val VALIDATION_DEBOUNCE_MS = 800L private const val HIDDEN_PLACEHOLDER = "" @@ -313,17 +313,29 @@ private fun ActionEditorDialog( domain = parsedDomain, auth = auth, rawSpec = rawSpec, - apiKey = if (authType == "service_http" && apiKey.isNotBlank() && apiKey != HIDDEN_PLACEHOLDER) { + apiKey = if ( + authType == "service_http" && + apiKey.isNotBlank() && + apiKey != HIDDEN_PLACEHOLDER + ) { apiKey } else { null }, - oauthClientId = if (authType == "oauth" && oauthClientId.isNotBlank() && oauthClientId != HIDDEN_PLACEHOLDER) { + oauthClientId = if ( + authType == "oauth" && + oauthClientId.isNotBlank() && + oauthClientId != HIDDEN_PLACEHOLDER + ) { oauthClientId } else { null }, - oauthClientSecret = if (authType == "oauth" && oauthClientSecret.isNotBlank() && oauthClientSecret != HIDDEN_PLACEHOLDER) { + oauthClientSecret = if ( + authType == "oauth" && + oauthClientSecret.isNotBlank() && + oauthClientSecret != HIDDEN_PLACEHOLDER + ) { oauthClientSecret } else { null @@ -373,7 +385,8 @@ private fun ActionEditorDialog( label = { Text(stringResource(R.string.label_openapi_spec)) }, placeholder = { Text( - text = "{\n \"openapi\": \"3.0.0\",\n \"info\": { ... },\n \"servers\": [{ \"url\": \"...\" }],\n \"paths\": { ... }\n}", + text = "{\n \"openapi\": \"3.0.0\",\n" + + " \"info\": { ... },\n \"paths\": { ... }\n}", fontFamily = FontFamily.Monospace, ) }, diff --git a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentAdvancedPanel.kt b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentAdvancedPanel.kt index af86be1..8808fe0 100644 --- a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentAdvancedPanel.kt +++ b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentAdvancedPanel.kt @@ -26,11 +26,11 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp -import kotlin.math.roundToInt import com.librechat.android.feature.agents.R -import androidx.compose.ui.res.stringResource +import kotlin.math.roundToInt data class AgentAdvancedSettings( val temperature: Float? = null, diff --git a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentAvatarPicker.kt b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentAvatarPicker.kt index be7d872..a397286 100644 --- a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentAvatarPicker.kt +++ b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentAvatarPicker.kt @@ -18,12 +18,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import com.librechat.android.feature.agents.R -import androidx.compose.ui.res.stringResource @Composable fun AgentAvatarPicker( diff --git a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentCapabilitiesSection.kt b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentCapabilitiesSection.kt index ada2efd..c376ce0 100644 --- a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentCapabilitiesSection.kt +++ b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentCapabilitiesSection.kt @@ -15,10 +15,10 @@ 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.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import com.librechat.android.feature.agents.R -import androidx.compose.ui.res.stringResource data class AgentCapabilities( val artifacts: Boolean = false, diff --git a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentCategorySelector.kt b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentCategorySelector.kt index 7d7fe0a..4dee579 100644 --- a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentCategorySelector.kt +++ b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentCategorySelector.kt @@ -14,9 +14,9 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import com.librechat.android.core.model.AgentCategory import com.librechat.android.feature.agents.R -import androidx.compose.ui.res.stringResource /** * Maps category values to human-readable English labels. diff --git a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentCodeInterpreterSection.kt b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentCodeInterpreterSection.kt index 29b12ff..1fdb0fc 100644 --- a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentCodeInterpreterSection.kt +++ b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentCodeInterpreterSection.kt @@ -13,9 +13,9 @@ 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.stringResource import androidx.compose.ui.unit.dp import com.librechat.android.feature.agents.R -import androidx.compose.ui.res.stringResource @Composable fun AgentCodeInterpreterSection( diff --git a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentFileSearchSection.kt b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentFileSearchSection.kt index 03355a2..a947542 100644 --- a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentFileSearchSection.kt +++ b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentFileSearchSection.kt @@ -13,9 +13,9 @@ 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.stringResource import androidx.compose.ui.unit.dp import com.librechat.android.feature.agents.R -import androidx.compose.ui.res.stringResource @Composable fun AgentFileSearchSection( diff --git a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentHandoffConfig.kt b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentHandoffConfig.kt index 10e07bb..506c79a 100644 --- a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentHandoffConfig.kt +++ b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentHandoffConfig.kt @@ -35,10 +35,10 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.librechat.android.feature.agents.AgentHandoffDisplayData import com.librechat.android.feature.agents.R -import androidx.compose.ui.res.stringResource @Composable fun AgentHandoffConfig( diff --git a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentMcpToolsSelector.kt b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentMcpToolsSelector.kt index 26a2a06..5cff934 100644 --- a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentMcpToolsSelector.kt +++ b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentMcpToolsSelector.kt @@ -5,9 +5,7 @@ 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.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ExpandLess @@ -23,10 +21,10 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.librechat.android.core.model.mcp.McpTool import com.librechat.android.feature.agents.R -import androidx.compose.ui.res.stringResource /** Hierarchical MCP tool picker grouped by server with checkbox selection. */ @Composable @@ -57,7 +55,7 @@ fun AgentMcpToolsSelector( ) } - val unknownServerLabel = stringResource(R.string.unknown_server) + val unknownServerLabel = stringResource(R.string.unknown_server) AnimatedVisibility(visible = expanded) { Column( diff --git a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentModelPicker.kt b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentModelPicker.kt index 88a64b6..7727798 100644 --- a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentModelPicker.kt +++ b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentModelPicker.kt @@ -16,9 +16,9 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.librechat.android.feature.agents.R -import androidx.compose.ui.res.stringResource data class ModelOption( val id: String, diff --git a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentSharingSection.kt b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentSharingSection.kt index 291e094..f26bba0 100644 --- a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentSharingSection.kt +++ b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentSharingSection.kt @@ -29,9 +29,9 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.librechat.android.feature.agents.R -import androidx.compose.ui.res.stringResource enum class AgentVisibility(val label: String) { PRIVATE("Private"), diff --git a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentSupportContactSection.kt b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentSupportContactSection.kt index eddc3a5..792068d 100644 --- a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentSupportContactSection.kt +++ b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentSupportContactSection.kt @@ -9,9 +9,9 @@ import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.librechat.android.feature.agents.R -import androidx.compose.ui.res.stringResource @androidx.compose.runtime.Immutable data class SupportContactState( diff --git a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentVersionHistory.kt b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentVersionHistory.kt index 0b2235e..207e8a9 100644 --- a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentVersionHistory.kt +++ b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/AgentVersionHistory.kt @@ -21,9 +21,9 @@ import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.librechat.android.feature.agents.R -import androidx.compose.ui.res.stringResource data class AgentVersion( val version: Int, diff --git a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/ToolSelectDialog.kt b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/ToolSelectDialog.kt index 6eb6988..a12773d 100644 --- a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/ToolSelectDialog.kt +++ b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/components/ToolSelectDialog.kt @@ -42,6 +42,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog @@ -49,7 +50,6 @@ import androidx.compose.ui.window.DialogProperties import coil.compose.AsyncImage import com.librechat.android.feature.agents.AgentToolDisplayData import com.librechat.android.feature.agents.R -import androidx.compose.ui.res.stringResource /** * Full-screen dialog for selecting agent tools, matching the web app's ToolSelectDialog. diff --git a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/screen/AgentDetailScreen.kt b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/screen/AgentDetailScreen.kt index 2e7892b..925c0e7 100644 --- a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/screen/AgentDetailScreen.kt +++ b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/screen/AgentDetailScreen.kt @@ -42,20 +42,19 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp -import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.librechat.android.core.ui.R as CoreUiR import com.librechat.android.core.ui.components.AvatarImage import com.librechat.android.core.ui.components.ErrorBanner import com.librechat.android.core.ui.components.LoadingIndicator -import com.librechat.android.feature.agents.AgentDetailDisplayData +import com.librechat.android.feature.agents.R import com.librechat.android.feature.agents.viewmodel.AgentDetailEvent import com.librechat.android.feature.agents.viewmodel.AgentDetailViewModel -import com.librechat.android.feature.agents.R -import androidx.compose.ui.res.stringResource +import org.koin.compose.viewmodel.koinViewModel +import com.librechat.android.core.ui.R as CoreUiR @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @Composable @@ -228,7 +227,11 @@ fun AgentDetailScreen( Spacer(modifier = Modifier.height(4.dp)) Text( - text = stringResource(R.string.by_author, agent.authorName ?: agent.author ?: stringResource(R.string.unknown_author)), + text = stringResource( + R.string.by_author, + agent.authorName ?: agent.author + ?: stringResource(R.string.unknown_author), + ), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -354,9 +357,11 @@ private fun AgentDetailSection( Text( text = content, style = MaterialTheme.typography.bodyLarge, - color = if (dimContent) MaterialTheme.colorScheme.onSurfaceVariant - else MaterialTheme.colorScheme.onSurface, + color = if (dimContent) { + MaterialTheme.colorScheme.onSurfaceVariant + } else { + MaterialTheme.colorScheme.onSurface + }, ) } } - diff --git a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/screen/AgentEditorScreen.kt b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/screen/AgentEditorScreen.kt index 9618b56..6172ba2 100644 --- a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/screen/AgentEditorScreen.kt +++ b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/screen/AgentEditorScreen.kt @@ -48,11 +48,12 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.librechat.android.core.ui.components.ErrorBanner import com.librechat.android.core.ui.components.LoadingIndicator +import com.librechat.android.feature.agents.R import com.librechat.android.feature.agents.components.AgentActionsPanel import com.librechat.android.feature.agents.components.AgentAdvancedPanel import com.librechat.android.feature.agents.components.AgentAvatarPicker @@ -68,8 +69,7 @@ import com.librechat.android.feature.agents.components.AgentVersionHistory import com.librechat.android.feature.agents.components.ToolSelectDialog import com.librechat.android.feature.agents.viewmodel.AgentEditorEvent import com.librechat.android.feature.agents.viewmodel.AgentEditorViewModel -import com.librechat.android.feature.agents.R -import androidx.compose.ui.res.stringResource +import org.koin.compose.viewmodel.koinViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/screen/AgentMarketplaceScreen.kt b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/screen/AgentMarketplaceScreen.kt index b174db4..ea11330 100644 --- a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/screen/AgentMarketplaceScreen.kt +++ b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/screen/AgentMarketplaceScreen.kt @@ -41,20 +41,20 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.librechat.android.core.ui.R as CoreUiR import com.librechat.android.core.ui.components.AvatarImage import com.librechat.android.core.ui.components.EmptyState import com.librechat.android.core.ui.components.ErrorBanner import com.librechat.android.core.ui.components.LibreChatTopBar import com.librechat.android.core.ui.components.LoadingIndicator import com.librechat.android.feature.agents.AgentCardDisplayData -import com.librechat.android.feature.agents.viewmodel.AgentMarketplaceViewModel import com.librechat.android.feature.agents.R -import androidx.compose.ui.res.stringResource +import com.librechat.android.feature.agents.viewmodel.AgentMarketplaceViewModel +import org.koin.compose.viewmodel.koinViewModel +import com.librechat.android.core.ui.R as CoreUiR @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/util/OpenApiSpecParser.kt b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/util/OpenApiSpecParser.kt index 2500ac3..a732a3b 100644 --- a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/util/OpenApiSpecParser.kt +++ b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/util/OpenApiSpecParser.kt @@ -366,6 +366,7 @@ object OpenApiSpecParser { "Not valid JSON or YAML.\n" + "JSON error: ${jsonError.message}\n" + "YAML error: ${yamlError.message}", + yamlError, ) } } diff --git a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/viewmodel/AgentDetailViewModel.kt b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/viewmodel/AgentDetailViewModel.kt index 3daf0d0..420705c 100644 --- a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/viewmodel/AgentDetailViewModel.kt +++ b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/viewmodel/AgentDetailViewModel.kt @@ -130,8 +130,11 @@ class AgentDetailViewModel( private fun Agent.toDetailDisplayData(): AgentDetailDisplayData { val resolvedUrl = avatarUrl?.let { url -> - if (url.startsWith("http")) url - else "${serverDataStore.getBaseUrl()}$url" + if (url.startsWith("http")) { + url + } else { + "${serverDataStore.getBaseUrl()}$url" + } } return AgentDetailDisplayData( id = id, diff --git a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/viewmodel/AgentEditorViewModel.kt b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/viewmodel/AgentEditorViewModel.kt index f9b7f62..45aaa04 100644 --- a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/viewmodel/AgentEditorViewModel.kt +++ b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/viewmodel/AgentEditorViewModel.kt @@ -10,9 +10,8 @@ import com.librechat.android.core.common.result.Result import com.librechat.android.core.data.repository.AgentRepository import com.librechat.android.core.data.repository.ConfigRepository import com.librechat.android.core.data.repository.McpRepository -import com.librechat.android.core.model.Agent -import com.librechat.android.core.model.ActionAuth import com.librechat.android.core.model.ActionMetadata +import com.librechat.android.core.model.Agent import com.librechat.android.core.model.AgentAction import com.librechat.android.core.model.AgentCategory import com.librechat.android.core.model.AgentTool @@ -20,20 +19,20 @@ import com.librechat.android.core.model.SupportContact import com.librechat.android.core.model.mcp.McpTool import com.librechat.android.core.model.request.CreateActionRequest import com.librechat.android.core.model.request.CreateAgentRequest +import com.librechat.android.core.model.request.FunctionTool import com.librechat.android.core.model.request.RevertAgentRequest import com.librechat.android.core.model.request.UpdateAgentRequest -import com.librechat.android.core.model.request.FunctionTool import com.librechat.android.feature.agents.AgentActionDisplayData import com.librechat.android.feature.agents.AgentHandoffDisplayData import com.librechat.android.feature.agents.AgentToolDisplayData import com.librechat.android.feature.agents.components.AgentAdvancedSettings -import com.librechat.android.feature.agents.util.OpenApiSpecParser import com.librechat.android.feature.agents.components.AgentCapabilities import com.librechat.android.feature.agents.components.AgentSharingState import com.librechat.android.feature.agents.components.AgentVersion import com.librechat.android.feature.agents.components.AgentVisibility import com.librechat.android.feature.agents.components.ModelOption import com.librechat.android.feature.agents.components.SupportContactState +import com.librechat.android.feature.agents.util.OpenApiSpecParser import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow @@ -887,13 +886,13 @@ class AgentEditorViewModel( val serverName = matchingTool.serverName if (serverName != null) { // Store as "toolName_mcp_serverName" format - tools.add("${mcpToolName}${MCP_TOOL_SEPARATOR}${serverName}") + tools.add("${mcpToolName}${MCP_TOOL_SEPARATOR}$serverName") } else { tools.add(mcpToolName) } } else { // May be a server name marker - tools.add("${MCP_SERVER_MARKER}${MCP_TOOL_SEPARATOR}${mcpToolName}") + tools.add("${MCP_SERVER_MARKER}${MCP_TOOL_SEPARATOR}$mcpToolName") } } diff --git a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/viewmodel/AgentMarketplaceViewModel.kt b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/viewmodel/AgentMarketplaceViewModel.kt index 35f20c3..7c098e3 100644 --- a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/viewmodel/AgentMarketplaceViewModel.kt +++ b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/viewmodel/AgentMarketplaceViewModel.kt @@ -1,8 +1,8 @@ package com.librechat.android.feature.agents.viewmodel +import androidx.compose.runtime.Immutable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import androidx.compose.runtime.Immutable import com.librechat.android.core.common.result.Result import com.librechat.android.core.data.datastore.ServerDataStore import com.librechat.android.core.data.repository.AgentRepository @@ -47,8 +47,11 @@ class AgentMarketplaceViewModel( private fun Agent.toCardDisplayData(): AgentCardDisplayData { val resolvedUrl = avatarUrl?.let { url -> - if (url.startsWith("http")) url - else "${serverDataStore.getBaseUrl()}$url" + if (url.startsWith("http")) { + url + } else { + "${serverDataStore.getBaseUrl()}$url" + } } return AgentCardDisplayData( id = id, diff --git a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/ForgotPasswordScreen.kt b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/ForgotPasswordScreen.kt index f64f461..fbda978 100644 --- a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/ForgotPasswordScreen.kt +++ b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/ForgotPasswordScreen.kt @@ -23,14 +23,14 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp -import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.librechat.android.feature.auth.viewmodel.ForgotPasswordViewModel import com.librechat.android.feature.auth.R -import androidx.compose.ui.res.stringResource +import com.librechat.android.feature.auth.viewmodel.ForgotPasswordViewModel +import org.koin.compose.viewmodel.koinViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/LoginScreen.kt b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/LoginScreen.kt index 167813e..94eba17 100644 --- a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/LoginScreen.kt +++ b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/LoginScreen.kt @@ -21,26 +21,26 @@ import androidx.compose.runtime.getValue 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.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LifecycleEventEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.librechat.android.feature.auth.viewmodel.LoginViewModel import com.librechat.android.feature.auth.R -import androidx.compose.ui.res.stringResource +import com.librechat.android.feature.auth.viewmodel.LoginViewModel +import org.koin.compose.viewmodel.koinViewModel @Composable fun LoginScreen( onLoginSuccess: () -> Unit, onNavigateToRegister: () -> Unit, + modifier: Modifier = Modifier, onNavigateToForgotPassword: () -> Unit = {}, onNavigateToTwoFactor: (String) -> Unit = {}, - modifier: Modifier = Modifier, viewModel: LoginViewModel = koinViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() diff --git a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/RegisterScreen.kt b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/RegisterScreen.kt index 84f9ce6..c8a5f7f 100644 --- a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/RegisterScreen.kt +++ b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/RegisterScreen.kt @@ -20,15 +20,15 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.dp -import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.librechat.android.feature.auth.viewmodel.RegisterViewModel import com.librechat.android.feature.auth.R -import androidx.compose.ui.res.stringResource +import com.librechat.android.feature.auth.viewmodel.RegisterViewModel +import org.koin.compose.viewmodel.koinViewModel @Composable fun RegisterScreen( diff --git a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/ResetPasswordScreen.kt b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/ResetPasswordScreen.kt index c35b9ec..f203059 100644 --- a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/ResetPasswordScreen.kt +++ b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/ResetPasswordScreen.kt @@ -23,15 +23,15 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.dp -import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.librechat.android.feature.auth.viewmodel.ResetPasswordViewModel import com.librechat.android.feature.auth.R -import androidx.compose.ui.res.stringResource +import com.librechat.android.feature.auth.viewmodel.ResetPasswordViewModel +import org.koin.compose.viewmodel.koinViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/ServerUrlScreen.kt b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/ServerUrlScreen.kt index 2a58800..f9d757c 100644 --- a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/ServerUrlScreen.kt +++ b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/ServerUrlScreen.kt @@ -19,14 +19,14 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp -import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.librechat.android.feature.auth.viewmodel.ServerUrlViewModel import com.librechat.android.feature.auth.R -import androidx.compose.ui.res.stringResource +import com.librechat.android.feature.auth.viewmodel.ServerUrlViewModel +import org.koin.compose.viewmodel.koinViewModel @Composable fun ServerUrlScreen( diff --git a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/TermsScreen.kt b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/TermsScreen.kt index 038eef8..e93055b 100644 --- a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/TermsScreen.kt +++ b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/TermsScreen.kt @@ -26,15 +26,15 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.librechat.android.feature.auth.viewmodel.TermsViewModel import com.librechat.android.feature.auth.R -import androidx.compose.ui.res.stringResource +import com.librechat.android.feature.auth.viewmodel.TermsViewModel +import org.koin.compose.viewmodel.koinViewModel /** Displays server terms of service loaded from the backend; gates navigation on user acceptance. */ @OptIn(ExperimentalMaterial3Api::class) diff --git a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/TwoFactorScreen.kt b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/TwoFactorScreen.kt index 7f09861..9c6e980 100644 --- a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/TwoFactorScreen.kt +++ b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/TwoFactorScreen.kt @@ -31,16 +31,16 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.librechat.android.feature.auth.viewmodel.TwoFactorViewModel import com.librechat.android.feature.auth.R -import androidx.compose.ui.res.stringResource +import com.librechat.android.feature.auth.viewmodel.TwoFactorViewModel +import org.koin.compose.viewmodel.koinViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -81,7 +81,11 @@ fun TwoFactorScreen( verticalArrangement = Arrangement.Center, ) { Text( - text = if (uiState.isBackupMode) stringResource(R.string.enter_backup_code) else stringResource(R.string.enter_verification_code), + text = if (uiState.isBackupMode) { + stringResource(R.string.enter_backup_code) + } else { + stringResource(R.string.enter_verification_code) + }, style = MaterialTheme.typography.headlineSmall, modifier = Modifier.semantics { heading() }, ) diff --git a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/VerifyEmailScreen.kt b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/VerifyEmailScreen.kt index c2779d1..eeef00c 100644 --- a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/VerifyEmailScreen.kt +++ b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/VerifyEmailScreen.kt @@ -24,15 +24,15 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.librechat.android.feature.auth.viewmodel.VerifyEmailViewModel import com.librechat.android.feature.auth.R -import androidx.compose.ui.res.stringResource +import com.librechat.android.feature.auth.viewmodel.VerifyEmailViewModel +import org.koin.compose.viewmodel.koinViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/ForgotPasswordViewModel.kt b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/ForgotPasswordViewModel.kt index 001be4a..c7f203d 100644 --- a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/ForgotPasswordViewModel.kt +++ b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/ForgotPasswordViewModel.kt @@ -1,9 +1,9 @@ package com.librechat.android.feature.auth.viewmodel +import androidx.compose.runtime.Immutable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.librechat.android.core.common.result.Result -import androidx.compose.runtime.Immutable import com.librechat.android.core.data.repository.AuthRepository import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow diff --git a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/RegisterViewModel.kt b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/RegisterViewModel.kt index fdf1dde..2d11e78 100644 --- a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/RegisterViewModel.kt +++ b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/RegisterViewModel.kt @@ -1,10 +1,10 @@ package com.librechat.android.feature.auth.viewmodel +import androidx.compose.runtime.Immutable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.librechat.android.core.common.result.Result import com.librechat.android.core.data.repository.AuthRepository -import androidx.compose.runtime.Immutable import com.librechat.android.core.data.repository.ConfigRepository import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow diff --git a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/ResetPasswordViewModel.kt b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/ResetPasswordViewModel.kt index 603ca6b..7082461 100644 --- a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/ResetPasswordViewModel.kt +++ b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/ResetPasswordViewModel.kt @@ -1,10 +1,10 @@ package com.librechat.android.feature.auth.viewmodel +import androidx.compose.runtime.Immutable import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.librechat.android.core.common.result.Result -import androidx.compose.runtime.Immutable import com.librechat.android.core.data.repository.AuthRepository import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow diff --git a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/ServerUrlViewModel.kt b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/ServerUrlViewModel.kt index 2b7f785..62c8caf 100644 --- a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/ServerUrlViewModel.kt +++ b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/ServerUrlViewModel.kt @@ -1,10 +1,10 @@ package com.librechat.android.feature.auth.viewmodel +import androidx.compose.runtime.Immutable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.librechat.android.core.common.result.Result import com.librechat.android.core.data.datastore.ServerDataStore -import androidx.compose.runtime.Immutable import com.librechat.android.core.data.repository.ConfigRepository import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow diff --git a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/TermsViewModel.kt b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/TermsViewModel.kt index 11a4327..6e1591d 100644 --- a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/TermsViewModel.kt +++ b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/TermsViewModel.kt @@ -1,9 +1,9 @@ package com.librechat.android.feature.auth.viewmodel +import androidx.compose.runtime.Immutable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.librechat.android.core.common.result.Result -import androidx.compose.runtime.Immutable import com.librechat.android.core.data.repository.UserRepository import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow diff --git a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/TwoFactorViewModel.kt b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/TwoFactorViewModel.kt index 37596bb..ef7aab4 100644 --- a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/TwoFactorViewModel.kt +++ b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/TwoFactorViewModel.kt @@ -1,10 +1,10 @@ package com.librechat.android.feature.auth.viewmodel +import androidx.compose.runtime.Immutable import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.librechat.android.core.common.result.Result -import androidx.compose.runtime.Immutable import com.librechat.android.core.data.repository.AuthRepository import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow diff --git a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/VerifyEmailViewModel.kt b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/VerifyEmailViewModel.kt index 5fec2b3..52b8bf9 100644 --- a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/VerifyEmailViewModel.kt +++ b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/VerifyEmailViewModel.kt @@ -1,9 +1,9 @@ package com.librechat.android.feature.auth.viewmodel +import androidx.compose.runtime.Immutable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.librechat.android.core.common.result.Result -import androidx.compose.runtime.Immutable import com.librechat.android.core.data.repository.UserRepository import kotlinx.coroutines.Job import kotlinx.coroutines.delay diff --git a/feature/auth/src/test/kotlin/com/librechat/android/feature/auth/viewmodel/RegisterViewModelTest.kt b/feature/auth/src/test/kotlin/com/librechat/android/feature/auth/viewmodel/RegisterViewModelTest.kt index 2290b0b..8d26a49 100644 --- a/feature/auth/src/test/kotlin/com/librechat/android/feature/auth/viewmodel/RegisterViewModelTest.kt +++ b/feature/auth/src/test/kotlin/com/librechat/android/feature/auth/viewmodel/RegisterViewModelTest.kt @@ -228,7 +228,7 @@ class RegisterViewModelTest { viewModel.onNameChanged("Test") viewModel.onEmailChanged("test@test.com") viewModel.onUsernameChanged("testuser") - viewModel.onPasswordChanged("short1234") // 9 chars, less than 12 + viewModel.onPasswordChanged("short1234") // 9 chars, less than 12 viewModel.onConfirmPasswordChanged("short1234") viewModel.register() advanceUntilIdle() diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/audio/VoiceRecorder.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/audio/VoiceRecorder.kt index be3e872..a971a20 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/audio/VoiceRecorder.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/audio/VoiceRecorder.kt @@ -3,7 +3,7 @@ package com.librechat.android.feature.chat.audio import android.content.Context import android.media.MediaRecorder import android.os.Build -import android.util.Log +import timber.log.Timber import java.io.File /** @@ -13,10 +13,6 @@ import java.io.File */ class VoiceRecorder(private val context: Context) { - companion object { - private const val TAG = "VoiceRecorder" - } - private var mediaRecorder: MediaRecorder? = null private var outputFile: File? = null private var isCurrentlyRecording = false @@ -64,7 +60,7 @@ class VoiceRecorder(private val context: Context) { mediaRecorder = recorder isCurrentlyRecording = true } catch (e: Exception) { - Log.e(TAG, "Failed to start voice recording", e) + Timber.e(e, "Failed to start voice recording") cleanup() throw e } @@ -87,7 +83,7 @@ class VoiceRecorder(private val context: Context) { outputFile = null bytes } catch (e: Exception) { - Log.e(TAG, "Failed to stop voice recording", e) + Timber.e(e, "Failed to stop voice recording") cleanup() null } diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/AgentHandoffCard.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/AgentHandoffCard.kt index 51df160..4708545 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/AgentHandoffCard.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/AgentHandoffCard.kt @@ -21,12 +21,12 @@ 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.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.compose.ui.res.stringResource import com.librechat.android.feature.chat.R /** @@ -48,7 +48,8 @@ fun AgentHandoffCard( modifier: Modifier = Modifier, ) { val unknownAgent = stringResource(R.string.agent_unknown) - val handoffCd = stringResource(R.string.cd_agent_handoff, handoff.fromAgent ?: unknownAgent, handoff.toAgent ?: unknownAgent) + val handoffCd = + stringResource(R.string.cd_agent_handoff, handoff.fromAgent ?: unknownAgent, handoff.toAgent ?: unknownAgent) Card( modifier = modifier .fillMaxWidth() diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/AttachmentChipsRow.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/AttachmentChipsRow.kt index 254c13b..56aefd9 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/AttachmentChipsRow.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/AttachmentChipsRow.kt @@ -19,12 +19,12 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.InsertDriveFile import androidx.compose.material.icons.filled.AttachFile import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Code -import androidx.compose.material.icons.filled.FindInPage -import androidx.compose.material.icons.automirrored.filled.InsertDriveFile import androidx.compose.material.icons.filled.Extension +import androidx.compose.material.icons.filled.FindInPage import androidx.compose.material.icons.filled.TravelExplore import androidx.compose.material3.AssistChip import androidx.compose.material3.AssistChipDefaults @@ -43,6 +43,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.role @@ -52,7 +53,6 @@ import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import com.librechat.android.core.common.ToolConstants import com.librechat.android.feature.chat.McpServerDisplayData -import androidx.compose.ui.res.stringResource import com.librechat.android.feature.chat.R /** @@ -65,11 +65,9 @@ fun AttachmentChipsRow( attachedFiles: List, enabledTools: Set, onRemoveFile: (AttachedFile) -> Unit, - onToggleTool: (String) -> Unit, modifier: Modifier = Modifier, mcpServers: List = emptyList(), selectedMcpServerNames: Set = emptySet(), - onToggleMcpServer: (String) -> Unit = {}, ) { val hasFiles = attachedFiles.isNotEmpty() val hasWebSearch = ToolConstants.WEB_SEARCH in enabledTools diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/AudioContent.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/AudioContent.kt index bd73780..dc3a9ed 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/AudioContent.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/AudioContent.kt @@ -18,7 +18,6 @@ import androidx.compose.ui.viewinterop.AndroidView import androidx.media3.common.MediaItem import androidx.media3.common.util.UnstableApi import androidx.media3.datasource.ByteArrayDataSource -import androidx.media3.datasource.DataSpec import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.source.ProgressiveMediaSource import androidx.media3.ui.PlayerView @@ -50,7 +49,6 @@ fun AudioContent( } val dataSource = ByteArrayDataSource(audioBytes) - val dataSpec = DataSpec(android.net.Uri.parse("data:$mimeType;base64,")) ExoPlayer.Builder(context).build().apply { val mediaSource = ProgressiveMediaSource.Factory { dataSource } diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/AudioContentPlayer.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/AudioContentPlayer.kt index d35896b..beb44f4 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/AudioContentPlayer.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/AudioContentPlayer.kt @@ -1,7 +1,6 @@ package com.librechat.android.feature.chat.components import android.media.MediaPlayer -import android.util.Log import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -33,12 +32,13 @@ 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.stringResource import androidx.compose.ui.unit.dp +import com.librechat.android.feature.chat.R import kotlinx.coroutines.delay +import timber.log.Timber import java.io.File import java.util.Locale -import androidx.compose.ui.res.stringResource -import com.librechat.android.feature.chat.R /** Plays audio via MediaPlayer with play/pause and seekbar. Polls progress every 250ms. Releases on dispose. */ @Composable @@ -66,7 +66,7 @@ fun AudioContentPlayer( durationMs = it.duration } setOnErrorListener { _, what, extra -> - Log.e("AudioContentPlayer", "MediaPlayer error: what=$what, extra=$extra") + Timber.e("MediaPlayer error: what=$what, extra=$extra") isPlaying = false true } @@ -81,7 +81,7 @@ fun AudioContentPlayer( mediaPlayer.setDataSource(audioUrl) mediaPlayer.prepareAsync() } catch (e: Exception) { - Log.e("AudioContentPlayer", "Failed to set data source", e) + Timber.e(e, "Failed to set data source") } } diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/ChatInput.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/ChatInput.kt index f313a35..2619244 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/ChatInput.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/ChatInput.kt @@ -33,7 +33,6 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Send import androidx.compose.material.icons.filled.Add @@ -59,29 +58,29 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color import androidx.compose.ui.draw.alpha import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext -import androidx.core.content.ContextCompat -import androidx.core.content.FileProvider +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat +import androidx.core.content.FileProvider import com.librechat.android.feature.chat.McpServerDisplayData -import java.io.File import com.librechat.android.feature.chat.PromptMentionDisplayData -import androidx.compose.ui.res.stringResource import com.librechat.android.feature.chat.R +import java.io.File data class AttachedFile( val uri: Uri, @@ -212,7 +211,6 @@ fun ChatInput( } val focusRequester = remember { FocusRequester() } - val clipboardManager = LocalClipboardManager.current var showToolsSheet by remember { mutableStateOf(false) } // Use TextFieldValue internally so we can control cursor position. @@ -256,10 +254,8 @@ fun ChatInput( attachedFiles = attachedFiles, enabledTools = enabledTools, onRemoveFile = onRemoveFile, - onToggleTool = onToggleTool, mcpServers = mcpServers, selectedMcpServerNames = selectedMcpServerNames, - onToggleMcpServer = onToggleMcpServer, ) Row( @@ -327,7 +323,9 @@ fun ChatInput( val afterAt = inputText.substring(atIndex + 1) // Only show suggestions if there's no space after @ (still typing the mention) if (!afterAt.contains(' ')) afterAt else null - } else null + } else { + null + } } } @@ -339,7 +337,9 @@ fun ChatInput( group.name.contains(query, ignoreCase = true) || group.command?.contains(query, ignoreCase = true) == true }.take(5) - } else emptyList() + } else { + emptyList() + } } } @@ -350,7 +350,9 @@ fun ChatInput( val afterSlash = inputText.substring(1) // Only show suggestions if there's no space (still typing the command) if (!afterSlash.contains(' ')) afterSlash else null - } else null + } else { + null + } } } @@ -362,7 +364,9 @@ fun ChatInput( val cmd = group.command cmd != null && cmd.contains(query, ignoreCase = true) }.take(5) - } else emptyList() + } else { + emptyList() + } } } diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/ChatInputToolbar.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/ChatInputToolbar.kt index 946971c..00d3b72 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/ChatInputToolbar.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/ChatInputToolbar.kt @@ -30,12 +30,12 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import com.librechat.android.core.common.ToolConstants import com.librechat.android.feature.chat.McpServerDisplayData -import androidx.compose.ui.res.stringResource import com.librechat.android.feature.chat.R data class ToolToggle( diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/ChatToolsBottomSheet.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/ChatToolsBottomSheet.kt index 9605598..ef6418f 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/ChatToolsBottomSheet.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/ChatToolsBottomSheet.kt @@ -16,10 +16,10 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material.icons.filled.AttachFile import androidx.compose.material.icons.filled.Code import androidx.compose.material.icons.filled.Extension -import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material.icons.filled.FindInPage import androidx.compose.material.icons.filled.PhotoCamera import androidx.compose.material.icons.filled.PhotoLibrary @@ -46,6 +46,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.role @@ -54,7 +55,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.librechat.android.core.common.ToolConstants import com.librechat.android.feature.chat.McpServerDisplayData -import androidx.compose.ui.res.stringResource import com.librechat.android.feature.chat.R @OptIn(ExperimentalMaterial3Api::class) @@ -72,6 +72,7 @@ fun ChatToolsBottomSheet( onOpenModelSelector: () -> Unit, selectedModelDisplay: String?, onDismiss: () -> Unit, + modifier: Modifier = Modifier, isCodeInterpreterAvailable: Boolean = true, ) { val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) @@ -79,6 +80,7 @@ fun ChatToolsBottomSheet( ModalBottomSheet( onDismissRequest = onDismiss, + modifier = modifier, sheetState = sheetState, ) { Column( @@ -284,7 +286,6 @@ fun ChatToolsBottomSheet( } } } - } } } diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/CitationChip.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/CitationChip.kt index 29c15a4..81b4566 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/CitationChip.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/CitationChip.kt @@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.ClickableText -import androidx.compose.ui.unit.sp import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.OpenInNew import androidx.compose.material3.Icon @@ -24,6 +23,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString @@ -35,7 +35,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Popup import androidx.compose.ui.window.PopupProperties -import androidx.compose.ui.res.stringResource import com.librechat.android.feature.chat.R /** @@ -135,8 +134,8 @@ fun parseCitations(text: String): Pair> { @Composable fun CitationText( text: String, - fontSizeMultiplier: Float = 1.0f, modifier: Modifier = Modifier, + fontSizeMultiplier: Float = 1.0f, ) { val (annotatedString, citations) = remember(text) { parseCitations(text) } var activeCitation by remember { mutableStateOf(null) } @@ -160,11 +159,14 @@ fun CitationText( } val baseStyle = MaterialTheme.typography.bodyLarge.let { style -> - if (fontSizeMultiplier == 1.0f) style - else style.copy( - fontSize = (style.fontSize.value * fontSizeMultiplier).sp, - lineHeight = (style.lineHeight.value * fontSizeMultiplier).sp, - ) + if (fontSizeMultiplier == 1.0f) { + style + } else { + style.copy( + fontSize = (style.fontSize.value * fontSizeMultiplier).sp, + lineHeight = (style.lineHeight.value * fontSizeMultiplier).sp, + ) + } } if (citations.isEmpty()) { diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/CodeBlock.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/CodeBlock.kt index 3b35735..05758a1 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/CodeBlock.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/CodeBlock.kt @@ -36,19 +36,19 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.withStyle -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import kotlinx.coroutines.delay -import androidx.compose.ui.res.stringResource import com.librechat.android.feature.chat.R +import kotlinx.coroutines.delay @Composable fun CodeBlock( @@ -232,7 +232,6 @@ private val keywordStyle = SpanStyle(color = Color(0xFF569CD6)) private val numberStyle = SpanStyle(color = Color(0xFFB5CEA8)) private val typeStyle = SpanStyle(color = Color(0xFF4EC9B0)) private val functionStyle = SpanStyle(color = Color(0xFFDCDCAA)) -private val punctuationStyle = SpanStyle(color = Color(0xFFD4D4D4)) private val annotationStyle = SpanStyle(color = Color(0xFFD7BA7D)) private val constantStyle = SpanStyle(color = Color(0xFF4FC1FF)) diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/CodeExecutionCard.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/CodeExecutionCard.kt index 274fb99..29c81e2 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/CodeExecutionCard.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/CodeExecutionCard.kt @@ -15,8 +15,8 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.Cancel +import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.Code import androidx.compose.material.icons.filled.ExpandLess import androidx.compose.material.icons.filled.ExpandMore @@ -35,13 +35,13 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp -import androidx.compose.ui.res.stringResource import com.librechat.android.feature.chat.R /** @@ -118,7 +118,14 @@ fun CodeExecutionCard( val isSuccess = exitCode == 0 Icon( imageVector = if (isSuccess) Icons.Default.CheckCircle else Icons.Default.Cancel, - contentDescription = stringResource(if (isSuccess) R.string.cd_exit_code_success else R.string.cd_exit_code_failure, exitCode), + contentDescription = stringResource( + if (isSuccess) { + R.string.cd_exit_code_success + } else { + R.string.cd_exit_code_failure + }, + exitCode, + ), modifier = Modifier.size(18.dp), tint = if (isSuccess) { MaterialTheme.colorScheme.primary diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/ComparisonDualPane.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/ComparisonDualPane.kt index 378949e..9402ff0 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/ComparisonDualPane.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/ComparisonDualPane.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -17,7 +16,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.librechat.android.feature.chat.R @@ -36,9 +34,9 @@ fun ComparisonDualPane( secondaryModelSelector: @Composable () -> Unit, primaryContent: @Composable () -> Unit, secondaryContent: @Composable () -> Unit, + modifier: Modifier = Modifier, onContinueWithPrimary: (() -> Unit)? = null, onContinueWithSecondary: (() -> Unit)? = null, - modifier: Modifier = Modifier, ) { Row(modifier = modifier.fillMaxSize()) { // Primary pane diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/ComparisonTabBar.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/ComparisonTabBar.kt index 2a2adbc..cbd8cc9 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/ComparisonTabBar.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/ComparisonTabBar.kt @@ -42,10 +42,10 @@ fun ComparisonTabBar( secondaryModelName: String, primaryContent: @Composable () -> Unit, secondaryContent: @Composable () -> Unit, + modifier: Modifier = Modifier, onContinueWithPrimary: (() -> Unit)? = null, onContinueWithSecondary: (() -> Unit)? = null, onTabChanged: ((Int) -> Unit)? = null, - modifier: Modifier = Modifier, ) { val pagerState = rememberPagerState(pageCount = { 2 }) val coroutineScope = rememberCoroutineScope() diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/ContentPartRenderer.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/ContentPartRenderer.kt index 6542d50..fbec98c 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/ContentPartRenderer.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/ContentPartRenderer.kt @@ -39,17 +39,19 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.LayoutCoordinates +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.unit.dp import coil.compose.SubcomposeAsyncImage import com.librechat.android.core.common.ToolConstants import com.librechat.android.core.model.AgentToolCall import com.librechat.android.core.model.ContentType import com.librechat.android.core.model.MessageContentPart +import com.librechat.android.feature.chat.R import com.librechat.android.feature.chat.components.artifact.ArtifactButton import com.librechat.android.feature.chat.components.artifact.ArtifactPanel import com.librechat.android.feature.chat.components.artifact.ArtifactSegment @@ -63,14 +65,13 @@ import kotlinx.serialization.json.intOrNull import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive -import androidx.compose.ui.res.stringResource -import com.librechat.android.feature.chat.R private val lenientJson = Json { ignoreUnknownKeys = true; isLenient = true } @Composable fun ContentPartRenderer( part: MessageContentPart, + modifier: Modifier = Modifier, baseUrl: String = "", fontSizeMultiplier: Float = 1.0f, useKatex: Boolean = false, @@ -79,7 +80,6 @@ fun ContentPartRenderer( searchQuery: String? = null, searchFocusedOccurrence: Int = -1, onFocusedOccurrencePositioned: ((LayoutCoordinates) -> Unit)? = null, - modifier: Modifier = Modifier, ) { val constrainedModifier = modifier.fillMaxWidth() when (part.type) { @@ -282,85 +282,84 @@ fun ContentPartRenderer( @Composable private fun TextContentPart( text: String, + modifier: Modifier = Modifier, fontSizeMultiplier: Float = 1.0f, useKatex: Boolean = false, searchQuery: String? = null, searchFocusedOccurrence: Int = -1, onFocusedOccurrencePositioned: ((LayoutCoordinates) -> Unit)? = null, - modifier: Modifier = Modifier, ) { if (text.isBlank()) return val segments = remember(text) { detectArtifacts(text) } val hasArtifacts = remember(segments) { segments.any { it is ArtifactSegment.ArtifactReference } } - if (!hasArtifacts) { - MarkdownContent( - text = text, - modifier = modifier, - fontSizeMultiplier = fontSizeMultiplier, - useKatex = useKatex, - searchQuery = searchQuery, - searchFocusedOccurrence = searchFocusedOccurrence, - onFocusedOccurrencePositioned = onFocusedOccurrencePositioned, - ) - return - } - - val versionMap = remember(segments) { groupArtifactVersions(segments) } - var activeArtifact by remember { - mutableStateOf(null) - } - Column(modifier = modifier) { - segments.forEach { segment -> - when (segment) { - is ArtifactSegment.Text -> { - MarkdownContent( - text = segment.text, - fontSizeMultiplier = fontSizeMultiplier, - useKatex = useKatex, - searchQuery = searchQuery, - searchFocusedOccurrence = searchFocusedOccurrence, - onFocusedOccurrencePositioned = onFocusedOccurrencePositioned, - ) - } - is ArtifactSegment.ArtifactReference -> { - val versions = versionMap[segment.artifact.identifier] ?: listOf(segment.artifact) - Spacer(modifier = Modifier.height(8.dp)) - ArtifactButton( - artifact = segment.artifact, - onClick = { activeArtifact = segment.artifact }, - versionCount = versions.size, - ) - Spacer(modifier = Modifier.height(8.dp)) + if (!hasArtifacts) { + MarkdownContent( + text = text, + fontSizeMultiplier = fontSizeMultiplier, + useKatex = useKatex, + searchQuery = searchQuery, + searchFocusedOccurrence = searchFocusedOccurrence, + onFocusedOccurrencePositioned = onFocusedOccurrencePositioned, + ) + } else { + val versionMap = remember(segments) { groupArtifactVersions(segments) } + var activeArtifact by remember { + mutableStateOf(null) + } + + segments.forEach { segment -> + when (segment) { + is ArtifactSegment.Text -> { + MarkdownContent( + text = segment.text, + fontSizeMultiplier = fontSizeMultiplier, + useKatex = useKatex, + searchQuery = searchQuery, + searchFocusedOccurrence = searchFocusedOccurrence, + onFocusedOccurrencePositioned = onFocusedOccurrencePositioned, + ) + } + is ArtifactSegment.ArtifactReference -> { + val versions = versionMap[segment.artifact.identifier] ?: listOf(segment.artifact) + Spacer(modifier = Modifier.height(8.dp)) + ArtifactButton( + artifact = segment.artifact, + onClick = { activeArtifact = segment.artifact }, + versionCount = versions.size, + ) + Spacer(modifier = Modifier.height(8.dp)) + } } } - } - } - activeArtifact?.let { artifact -> - val versions = versionMap[artifact.identifier] ?: listOf(artifact) - ArtifactPanel( - artifact = artifact, - onDismiss = { activeArtifact = null }, - versions = versions, - ) + activeArtifact?.let { artifact -> + val versions = versionMap[artifact.identifier] ?: listOf(artifact) + ArtifactPanel( + artifact = artifact, + onDismiss = { activeArtifact = null }, + versions = versions, + ) + } + } } } @Composable private fun ThinkingContentPart( thinkingText: String, + modifier: Modifier = Modifier, fontSizeMultiplier: Float = 1.0f, useKatex: Boolean = false, searchQuery: String? = null, searchFocusedOccurrence: Int = -1, onFocusedOccurrencePositioned: ((LayoutCoordinates) -> Unit)? = null, - modifier: Modifier = Modifier, ) { var isExpanded by remember { mutableStateOf(false) } - val thinkingToggleCd = stringResource(if (isExpanded) R.string.cd_collapse_thinking else R.string.cd_expand_thinking) + val thinkingToggleCd = + stringResource(if (isExpanded) R.string.cd_collapse_thinking else R.string.cd_expand_thinking) Column( modifier = modifier @@ -438,7 +437,8 @@ private fun ToolCallContentPart( shape = RoundedCornerShape(8.dp), ) { Column(modifier = Modifier.padding(12.dp)) { - val toolCallCd = stringResource(if (isExpanded) R.string.cd_collapse_tool_call else R.string.cd_expand_tool_call, toolName) + val toolCallCd = + stringResource(if (isExpanded) R.string.cd_collapse_tool_call else R.string.cd_expand_tool_call, toolName) Row( modifier = Modifier .fillMaxWidth() diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/FeedbackCommentDialog.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/FeedbackCommentDialog.kt index 6aec7e9..d9fdbed 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/FeedbackCommentDialog.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/FeedbackCommentDialog.kt @@ -16,9 +16,9 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.unit.dp -import androidx.compose.ui.res.stringResource import com.librechat.android.feature.chat.R /** diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/ForkOptionsBottomSheet.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/ForkOptionsBottomSheet.kt index eb32be0..cfcf3c0 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/ForkOptionsBottomSheet.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/ForkOptionsBottomSheet.kt @@ -28,9 +28,9 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.librechat.android.core.model.request.ForkOption -import androidx.compose.ui.res.stringResource import com.librechat.android.feature.chat.R @OptIn(ExperimentalMaterial3Api::class) @@ -38,12 +38,14 @@ import com.librechat.android.feature.chat.R fun ForkOptionsBottomSheet( onDismiss: () -> Unit, onFork: (option: String, splitAtTarget: Boolean) -> Unit, + modifier: Modifier = Modifier, ) { val sheetState = rememberModalBottomSheetState() var splitAtTarget by remember { mutableStateOf(false) } ModalBottomSheet( onDismissRequest = onDismiss, + modifier = modifier, sheetState = sheetState, ) { Column( diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/FullscreenImageViewer.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/FullscreenImageViewer.kt index 2e82493..81ce2ab 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/FullscreenImageViewer.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/FullscreenImageViewer.kt @@ -44,6 +44,7 @@ import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties @@ -52,17 +53,17 @@ import androidx.core.content.FileProvider import coil.compose.AsyncImage import coil.imageLoader import coil.request.ImageRequest +import com.librechat.android.feature.chat.R import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.File -import androidx.compose.ui.res.stringResource -import com.librechat.android.feature.chat.R @Composable fun FullscreenImageViewer( imageUrl: String, onDismiss: () -> Unit, + modifier: Modifier = Modifier, ) { val context = LocalContext.current val scope = rememberCoroutineScope() @@ -186,7 +187,7 @@ fun FullscreenImageViewer( properties = DialogProperties(usePlatformDefaultWidth = false), ) { Box( - modifier = Modifier + modifier = modifier .fillMaxSize() .background(Color.Black.copy(alpha = 0.9f)), ) { diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/ImageGenCard.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/ImageGenCard.kt index 48f0fb2..b4b5ee2 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/ImageGenCard.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/ImageGenCard.kt @@ -31,13 +31,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import coil.compose.SubcomposeAsyncImage -import androidx.compose.ui.res.stringResource import com.librechat.android.feature.chat.R data class ImageGenResult( @@ -50,8 +50,8 @@ data class ImageGenResult( @Composable fun ImageGenCard( result: ImageGenResult, - showDescription: Boolean = true, modifier: Modifier = Modifier, + showDescription: Boolean = true, ) { var showFullscreen by remember { mutableStateOf(false) } diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/InConvoSearchBar.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/InConvoSearchBar.kt index 301b267..9f904c1 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/InConvoSearchBar.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/InConvoSearchBar.kt @@ -25,15 +25,15 @@ import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp +import com.librechat.android.feature.chat.R import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.filter -import androidx.compose.ui.res.stringResource -import com.librechat.android.feature.chat.R /** * Search overlay bar for finding text within the current conversation. @@ -80,7 +80,7 @@ fun InConvoSearchBar( modifier = Modifier .weight(1f) .height(48.dp) -.semantics { + .semantics { contentDescription = searchCd }, placeholder = { diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/InlineEditInput.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/InlineEditInput.kt index 84be46e..50e689d 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/InlineEditInput.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/InlineEditInput.kt @@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Button @@ -21,10 +20,10 @@ import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.compose.ui.res.stringResource import com.librechat.android.feature.chat.R @Composable diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/LandingContent.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/LandingContent.kt index a72e9bc..f0be8e1 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/LandingContent.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/LandingContent.kt @@ -20,14 +20,14 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import java.util.Calendar -import androidx.compose.ui.res.stringResource import com.librechat.android.feature.chat.R +import java.util.Calendar @Composable fun LandingContent( diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/LatexBlock.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/LatexBlock.kt index 8530df7..e785881 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/LatexBlock.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/LatexBlock.kt @@ -6,7 +6,6 @@ import android.view.ViewGroup import android.webkit.SslErrorHandler import android.webkit.WebView import android.webkit.WebViewClient -import timber.log.Timber import androidx.compose.foundation.background import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.fillMaxWidth @@ -23,6 +22,7 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView +import timber.log.Timber /** * Escapes a LaTeX string for safe embedding inside a JavaScript string literal. diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/LogContentCard.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/LogContentCard.kt index 18fd4c8..949afb0 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/LogContentCard.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/LogContentCard.kt @@ -31,13 +31,13 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp -import androidx.compose.ui.res.stringResource import com.librechat.android.feature.chat.R data class LogContent( diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/MarkdownContent.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/MarkdownContent.kt index c62df87..b0227c6 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/MarkdownContent.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/MarkdownContent.kt @@ -44,30 +44,26 @@ import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.text.withStyle -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.isSpecified import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties +import com.librechat.android.feature.chat.R import com.mikepenz.markdown.m3.Markdown import com.mikepenz.markdown.m3.markdownColor import com.mikepenz.markdown.m3.markdownTypography -import androidx.compose.ui.res.stringResource -import com.librechat.android.feature.chat.R /** * Markdown content rendering with full CommonMark support. Uses a hybrid approach: @@ -238,8 +234,8 @@ fun MarkdownContent( private fun HighlightedTextSegment( content: String, searchQuery: String, - focusedOccurrence: Int = -1, modifier: Modifier = Modifier, + focusedOccurrence: Int = -1, fontSizeMultiplier: Float = 1.0f, onPositioned: ((LayoutCoordinates) -> Unit)? = null, ) { @@ -595,8 +591,11 @@ internal fun parseMarkdownSegments(text: String): List { // \(...\) is always LaTeX; $...$ needs heuristic check val dollarContent = match.groupValues[1] val parenContent = match.groupValues[2] - if (parenContent.isNotBlank()) true - else dollarContent.isNotBlank() && looksLikeLatex(dollarContent) + if (parenContent.isNotBlank()) { + true + } else { + dollarContent.isNotBlank() && looksLikeLatex(dollarContent) + } } .toList() diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/McpResourceCarousel.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/McpResourceCarousel.kt index b54009b..d45b068 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/McpResourceCarousel.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/McpResourceCarousel.kt @@ -22,12 +22,12 @@ 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.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.compose.ui.res.stringResource import com.librechat.android.feature.chat.R /** diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/McpServerSelector.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/McpServerSelector.kt index 255599c..a6cdc32 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/McpServerSelector.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/McpServerSelector.kt @@ -28,12 +28,12 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.librechat.android.feature.chat.McpServerDisplayData -import androidx.compose.ui.res.stringResource import com.librechat.android.feature.chat.R @Composable diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/MemoryArtifactCard.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/MemoryArtifactCard.kt index f88bfd4..cb38e10 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/MemoryArtifactCard.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/MemoryArtifactCard.kt @@ -22,12 +22,12 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.compose.ui.res.stringResource import com.librechat.android.feature.chat.R /** @@ -50,7 +50,8 @@ fun MemoryArtifactCard( ) { var isExpanded by remember { mutableStateOf(false) } - val memoryCd = stringResource(R.string.cd_memory_artifact, artifact.title ?: stringResource(R.string.memory_artifact_untitled)) + val memoryCd = + stringResource(R.string.cd_memory_artifact, artifact.title ?: stringResource(R.string.memory_artifact_untitled)) Card( modifier = modifier .fillMaxWidth() diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/MermaidDiagram.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/MermaidDiagram.kt index a3eceb2..9ee48de 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/MermaidDiagram.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/MermaidDiagram.kt @@ -1,14 +1,12 @@ package com.librechat.android.feature.chat.components import android.annotation.SuppressLint -import android.graphics.Color as AndroidColor import android.net.http.SslError import android.view.ViewGroup import android.webkit.SslErrorHandler import android.webkit.WebChromeClient import android.webkit.WebView import android.webkit.WebViewClient -import timber.log.Timber import androidx.compose.animation.AnimatedContent import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -44,12 +42,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties -import androidx.compose.ui.res.stringResource import com.librechat.android.feature.chat.R +import timber.log.Timber @Composable fun MermaidDiagram( diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/MessageBubble.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/MessageBubble.kt index cad19a9..9c5b350 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/MessageBubble.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/MessageBubble.kt @@ -1,12 +1,12 @@ package com.librechat.android.feature.chat.components +import androidx.annotation.DrawableRes import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.background import androidx.compose.foundation.clickable 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 @@ -39,17 +39,16 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.graphics.compositeOver +import androidx.compose.ui.layout.LayoutCoordinates +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.annotation.DrawableRes import com.librechat.android.core.common.ChatLayoutConstants import com.librechat.android.core.model.Message import com.librechat.android.core.ui.components.AvatarImage -import kotlinx.coroutines.delay -import androidx.compose.ui.res.stringResource import com.librechat.android.feature.chat.R +import kotlinx.coroutines.delay internal val BubbleShape = RoundedCornerShape(16.dp) @@ -278,7 +277,11 @@ private fun ThreadMessageBubble( if (showAvatars) { AvatarImage( imageUrl = if (isUser) userAvatarUrl else message.iconURL, - fallbackText = if (isUser) (userName ?: stringResource(R.string.sender_you)) else (message.sender ?: stringResource(R.string.sender_assistant)), + fallbackText = if (isUser) { + userName ?: stringResource(R.string.sender_you) + } else { + message.sender ?: stringResource(R.string.sender_assistant) + }, fallbackIconRes = if (!isUser && message.iconURL == null) endpointIconRes else null, showPersonIcon = isUser && userAvatarUrl == null, tintIcon = if (!isUser && message.iconURL == null) tintEndpointIcon else false, @@ -287,7 +290,11 @@ private fun ThreadMessageBubble( Spacer(modifier = Modifier.width(8.dp)) } Text( - text = if (isUser) (userName ?: stringResource(R.string.sender_you)) else (message.sender ?: stringResource(R.string.sender_assistant)), + text = if (isUser) { + userName ?: stringResource(R.string.sender_you) + } else { + message.sender ?: stringResource(R.string.sender_assistant) + }, style = MaterialTheme.typography.titleSmall.copy( fontWeight = FontWeight.SemiBold, ), @@ -526,7 +533,11 @@ private fun TwoSidedMessageBubble( verticalAlignment = Alignment.CenterVertically, ) { Text( - text = if (isUser) (userName ?: stringResource(R.string.sender_you)) else (message.sender ?: stringResource(R.string.sender_assistant)), + text = if (isUser) { + userName ?: stringResource(R.string.sender_you) + } else { + message.sender ?: stringResource(R.string.sender_assistant) + }, style = MaterialTheme.typography.labelSmall.copy( fontWeight = FontWeight.SemiBold, ), @@ -848,4 +859,3 @@ private fun MessageContentAndActions( } } } - diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/MessageFiles.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/MessageFiles.kt index ec35491..cd3bb27 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/MessageFiles.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/MessageFiles.kt @@ -18,10 +18,10 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.InsertDriveFile import androidx.compose.material.icons.filled.AttachFile import androidx.compose.material.icons.filled.BrokenImage import androidx.compose.material.icons.filled.Description -import androidx.compose.material.icons.automirrored.filled.InsertDriveFile import androidx.compose.material.icons.filled.PictureAsPdf import androidx.compose.material.icons.filled.TableChart import androidx.compose.material3.CircularProgressIndicator @@ -38,6 +38,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.role @@ -46,7 +47,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import coil.compose.SubcomposeAsyncImage import com.librechat.android.core.model.FileReference -import androidx.compose.ui.res.stringResource import com.librechat.android.feature.chat.R /** @@ -60,8 +60,8 @@ import com.librechat.android.feature.chat.R @Composable fun MessageFiles( files: List, - baseUrl: String = "", modifier: Modifier = Modifier, + baseUrl: String = "", ) { if (files.isEmpty()) return diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/MessageList.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/MessageList.kt index bb0c38a..ac35b2d 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/MessageList.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/MessageList.kt @@ -1,5 +1,6 @@ package com.librechat.android.feature.chat.components +import androidx.annotation.DrawableRes import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -28,24 +29,23 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.boundsInRoot import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.annotation.DrawableRes import com.librechat.android.core.common.ChatLayoutConstants import com.librechat.android.core.model.FeedbackRating +import com.librechat.android.feature.chat.R import com.librechat.android.feature.chat.util.MessageNode import com.librechat.android.feature.chat.viewmodel.ActiveToolCall import com.librechat.android.feature.chat.viewmodel.SearchMatch import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import androidx.compose.ui.input.pointer.PointerEventPass -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.runtime.snapshotFlow -import androidx.compose.ui.res.stringResource -import com.librechat.android.feature.chat.R @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -53,11 +53,12 @@ fun MessageList( displayMessages: List, isStreaming: Boolean, streamingContent: String, - activeToolCalls: List = emptyList(), onSiblingNavigation: (parentMessageId: String, siblingIndex: Int) -> Unit, onEditMessage: (messageId: String) -> Unit, onRegenerateMessage: (messageId: String) -> Unit, onCopyMessage: (messageId: String) -> Unit, + modifier: Modifier = Modifier, + activeToolCalls: List = emptyList(), onFeedback: (messageId: String, rating: String?) -> Unit = { _, _ -> }, onContinue: (messageId: String) -> Unit = {}, onReadAloud: (messageId: String) -> Unit = {}, @@ -88,7 +89,6 @@ fun MessageList( currentSearchMatchIndex: Int = 0, searchScrollToIndex: Int? = null, onSearchScrollHandled: () -> Unit = {}, - modifier: Modifier = Modifier, ) { val listState = rememberLazyListState() val coroutineScope = rememberCoroutineScope() @@ -111,9 +111,12 @@ fun MessageList( val info = listState.layoutInfo val lastItem = info.visibleItemsInfo.lastOrNull() val itemCount = info.totalItemsCount - if (lastItem == null || itemCount == 0) true - else lastItem.index >= itemCount - 2 && - (lastItem.offset + lastItem.size - info.viewportEndOffset) < nearBottomThresholdPx + if (lastItem == null || itemCount == 0) { + true + } else { + lastItem.index >= itemCount - 2 && + (lastItem.offset + lastItem.size - info.viewportEndOffset) < nearBottomThresholdPx + } } } @@ -314,7 +317,7 @@ fun MessageList( key = { _, node -> node.message.messageId }, contentType = { _, node -> val role = if (node.message.isCreatedByUser) "user" else "assistant" - "${chatLayoutStyle}_${role}" + "${chatLayoutStyle}_$role" }, ) { index, node -> val feedbackRating = node.message.feedback?.rating @@ -384,24 +387,26 @@ fun MessageList( isSearchMatch = isMatch, isCurrentSearchMatch = isCurrent, searchFocusedOccurrence = focusedOccurrenceInMessage, - onFocusedOccurrencePositioned = if (isCurrent) { coordinates -> - if (!pendingFineTuneScroll) return@MessageBubble - pendingFineTuneScroll = false + onFocusedOccurrencePositioned = if (isCurrent) { + { coordinates -> + if (!pendingFineTuneScroll) return@MessageBubble + pendingFineTuneScroll = false - val layoutInfo = listState.layoutInfo - val viewportTop = layoutInfo.viewportStartOffset.toFloat() - val viewportBottom = layoutInfo.viewportEndOffset.toFloat() + val layoutInfo = listState.layoutInfo + val viewportTop = layoutInfo.viewportStartOffset.toFloat() + val viewportBottom = layoutInfo.viewportEndOffset.toFloat() - val segmentBounds = coordinates.boundsInRoot() - val padding = 80f // Extra padding so the highlight isn't flush against edges + val segmentBounds = coordinates.boundsInRoot() + val padding = 80f // Extra padding so the highlight isn't flush against edges - coroutineScope.launch { - if (segmentBounds.top < viewportTop + padding) { - // Segment is above or too close to the top of the viewport - listState.animateScrollBy(segmentBounds.top - viewportTop - padding) - } else if (segmentBounds.bottom > viewportBottom - padding) { - // Segment is below or too close to the bottom of the viewport - listState.animateScrollBy(segmentBounds.bottom - viewportBottom + padding) + coroutineScope.launch { + if (segmentBounds.top < viewportTop + padding) { + // Segment is above or too close to the top of the viewport + listState.animateScrollBy(segmentBounds.top - viewportTop - padding) + } else if (segmentBounds.bottom > viewportBottom - padding) { + // Segment is below or too close to the bottom of the viewport + listState.animateScrollBy(segmentBounds.bottom - viewportBottom + padding) + } } } } else { diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/ModelSelectorButton.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/ModelSelectorButton.kt index bee5e9f..90ff6af 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/ModelSelectorButton.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/ModelSelectorButton.kt @@ -13,9 +13,9 @@ import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.compose.ui.res.stringResource import com.librechat.android.feature.chat.R @Composable diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/ModelSelectorSheet.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/ModelSelectorSheet.kt index 10bcdc2..c3a527b 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/ModelSelectorSheet.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/ModelSelectorSheet.kt @@ -44,6 +44,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import com.librechat.android.core.common.EndpointConstants @@ -51,9 +52,8 @@ import com.librechat.android.core.model.Agent import com.librechat.android.core.model.EndpointConfig import com.librechat.android.core.ui.components.endpointIconRes import com.librechat.android.core.ui.components.isMonochromeEndpointIcon -import me.xdrop.fuzzywuzzy.FuzzySearch -import androidx.compose.ui.res.stringResource import com.librechat.android.feature.chat.R +import me.xdrop.fuzzywuzzy.FuzzySearch private val IconSize = 20.dp private const val FUZZY_MATCH_THRESHOLD = 55 @@ -118,8 +118,9 @@ fun ModelSelectorSheet( // Filter agents by search query (fuzzy matching), sorted by score when searching val filteredAgents = remember(agents, searchQuery) { - if (!isSearching) agents - else if (searchQuery.length <= 2) { + if (!isSearching) { + agents + } else if (searchQuery.length <= 2) { agents.filter { agent -> val name = agent.name ?: agent.id fuzzyMatches(name, searchQuery) diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/PresetPicker.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/PresetPicker.kt index 293b159..8852112 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/PresetPicker.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/PresetPicker.kt @@ -25,10 +25,10 @@ import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.librechat.android.feature.chat.PresetDisplayData -import androidx.compose.ui.res.stringResource import com.librechat.android.feature.chat.R @OptIn(ExperimentalMaterial3Api::class) diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/SavePresetDialog.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/SavePresetDialog.kt index d48e3ea..28fdb97 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/SavePresetDialog.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/SavePresetDialog.kt @@ -15,8 +15,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import com.librechat.android.feature.chat.R @Composable diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/SecondaryMessageList.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/SecondaryMessageList.kt index 50c8cb4..c525880 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/SecondaryMessageList.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/SecondaryMessageList.kt @@ -1,5 +1,6 @@ package com.librechat.android.feature.chat.components +import androidx.annotation.DrawableRes import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize @@ -15,7 +16,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.annotation.DrawableRes import com.librechat.android.core.common.ChatLayoutConstants import com.librechat.android.feature.chat.R import com.librechat.android.feature.chat.util.MessageNode @@ -31,6 +31,7 @@ fun SecondaryMessageList( displayMessages: List, isStreaming: Boolean, streamingContent: String, + modifier: Modifier = Modifier, activeToolCalls: List = emptyList(), error: String? = null, baseUrl: String = "", @@ -43,7 +44,6 @@ fun SecondaryMessageList( showAvatars: Boolean = true, showBubbles: Boolean = false, useKatex: Boolean = false, - modifier: Modifier = Modifier, ) { val listState = rememberLazyListState() val totalItemCount = displayMessages.size + diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/SiblingNavigator.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/SiblingNavigator.kt index 404e693..8786bc1 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/SiblingNavigator.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/SiblingNavigator.kt @@ -13,10 +13,10 @@ 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.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.stateDescription import androidx.compose.ui.unit.dp -import androidx.compose.ui.res.stringResource import com.librechat.android.feature.chat.R @Composable diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/StreamingIndicator.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/StreamingIndicator.kt index 84da0e0..907e0cd 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/StreamingIndicator.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/StreamingIndicator.kt @@ -13,12 +13,12 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.compose.ui.res.stringResource import com.librechat.android.feature.chat.R /** diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/StreamingMessageBubble.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/StreamingMessageBubble.kt index 05d694e..9d352e6 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/StreamingMessageBubble.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/StreamingMessageBubble.kt @@ -1,5 +1,6 @@ package com.librechat.android.feature.chat.components +import androidx.annotation.DrawableRes import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -20,7 +21,6 @@ import androidx.compose.ui.semantics.liveRegion import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.annotation.DrawableRes import com.librechat.android.core.common.ChatLayoutConstants import com.librechat.android.core.ui.components.AvatarImage @@ -39,6 +39,7 @@ fun StreamingMessageBubble( streamingContent: String, senderName: String, senderIconUrl: String?, + modifier: Modifier = Modifier, fontSizeMultiplier: Float = 1.0f, @DrawableRes endpointIconRes: Int? = null, tintEndpointIcon: Boolean = false, @@ -46,7 +47,6 @@ fun StreamingMessageBubble( showAvatars: Boolean = true, showBubbles: Boolean = false, useKatex: Boolean = false, - modifier: Modifier = Modifier, ) { if (chatLayoutStyle == ChatLayoutConstants.TWO_SIDED) { TwoSidedStreamingBubble( diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/StreamingToolCallCard.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/StreamingToolCallCard.kt index 7b2db06..96d3a10 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/StreamingToolCallCard.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/StreamingToolCallCard.kt @@ -31,10 +31,10 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import com.librechat.android.feature.chat.viewmodel.ActiveToolCall import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import com.librechat.android.feature.chat.R +import com.librechat.android.feature.chat.viewmodel.ActiveToolCall @Composable fun StreamingToolCallCard( @@ -56,8 +56,11 @@ fun StreamingToolCallCard( modifier = Modifier .fillMaxWidth() .then( - if (canExpand) Modifier.clickable { isExpanded = !isExpanded } - else Modifier, + if (canExpand) { + Modifier.clickable { isExpanded = !isExpanded } + } else { + Modifier + }, ), verticalAlignment = Alignment.CenterVertically, ) { diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/VideoContent.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/VideoContent.kt index bee5b2e..ac97b1e 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/VideoContent.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/VideoContent.kt @@ -23,12 +23,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.media3.common.MediaItem import androidx.media3.exoplayer.ExoPlayer import androidx.media3.ui.PlayerView -import androidx.compose.ui.res.stringResource import com.librechat.android.feature.chat.R @Composable diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/VideoContentPlayer.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/VideoContentPlayer.kt index 542a9b4..d0693f8 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/VideoContentPlayer.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/VideoContentPlayer.kt @@ -24,13 +24,13 @@ 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.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.media3.common.MediaItem import androidx.media3.common.Player import androidx.media3.exoplayer.ExoPlayer import androidx.media3.ui.PlayerView -import androidx.compose.ui.res.stringResource import com.librechat.android.feature.chat.R /** Plays video via ExoPlayer (media3) in a 16:9 card. Releases player on dispose via DisposableEffect. */ diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/WebSearchResultCard.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/WebSearchResultCard.kt index e5a31a0..e7c0bec 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/WebSearchResultCard.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/WebSearchResultCard.kt @@ -1,6 +1,5 @@ package com.librechat.android.feature.chat.components -import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -28,6 +27,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.role @@ -36,7 +36,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import coil.compose.SubcomposeAsyncImage -import androidx.compose.ui.res.stringResource import com.librechat.android.feature.chat.R /** diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/artifact/ArtifactButton.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/artifact/ArtifactButton.kt index 1e49c7e..a8d548a 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/artifact/ArtifactButton.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/artifact/ArtifactButton.kt @@ -12,13 +12,13 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Article import androidx.compose.material.icons.automirrored.filled.OpenInNew import androidx.compose.material.icons.filled.AccountTree -import androidx.compose.material.icons.automirrored.filled.Article import androidx.compose.material.icons.filled.Code import androidx.compose.material.icons.filled.Image -import androidx.compose.material.icons.filled.Widgets import androidx.compose.material.icons.filled.Web +import androidx.compose.material.icons.filled.Widgets import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.Icon @@ -28,10 +28,10 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.compose.ui.res.stringResource import com.librechat.android.feature.chat.R @Composable diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/artifact/ArtifactPanel.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/artifact/ArtifactPanel.kt index ea7a667..d1b5a16 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/artifact/ArtifactPanel.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/artifact/ArtifactPanel.kt @@ -19,8 +19,6 @@ 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.heightIn import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -45,11 +43,11 @@ import androidx.compose.material3.TabRow import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf 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 @@ -60,14 +58,14 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties +import com.librechat.android.feature.chat.R import com.librechat.android.feature.chat.components.CodeBlock import kotlinx.coroutines.launch -import androidx.compose.ui.res.stringResource -import com.librechat.android.feature.chat.R import timber.log.Timber @OptIn(ExperimentalMaterial3Api::class) diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/artifact/ArtifactVersionNav.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/artifact/ArtifactVersionNav.kt index b9dfd52..4c721d7 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/artifact/ArtifactVersionNav.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/components/artifact/ArtifactVersionNav.kt @@ -14,8 +14,8 @@ 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 androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import com.librechat.android.feature.chat.R /** Prev/next version navigation for artifacts. [currentIndex] is 0-based; display shows 1-based "v1/N". */ diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptDetailScreen.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptDetailScreen.kt index 31b851c..8f69cc8 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptDetailScreen.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptDetailScreen.kt @@ -28,11 +28,11 @@ 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.stringResource import androidx.compose.ui.unit.dp import com.librechat.android.core.ui.components.LibreChatTopBar -import com.librechat.android.feature.chat.prompts.components.PromptPreviewPanel -import androidx.compose.ui.res.stringResource import com.librechat.android.feature.chat.R +import com.librechat.android.feature.chat.prompts.components.PromptPreviewPanel @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -42,8 +42,8 @@ fun PromptDetailScreen( onDelete: () -> Unit, onUseInChat: (String) -> Unit, onEdit: (String) -> Unit, - onShare: () -> Unit = {}, modifier: Modifier = Modifier, + onShare: () -> Unit = {}, ) { Scaffold( modifier = modifier.fillMaxSize(), diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptEditorScreen.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptEditorScreen.kt index 0787b74..822593b 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptEditorScreen.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptEditorScreen.kt @@ -30,13 +30,13 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.unit.dp -import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.librechat.android.core.ui.components.LoadingIndicator -import androidx.compose.ui.res.stringResource import com.librechat.android.feature.chat.R +import org.koin.compose.viewmodel.koinViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptEditorViewModel.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptEditorViewModel.kt index 32ee3c3..1624280 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptEditorViewModel.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptEditorViewModel.kt @@ -1,19 +1,18 @@ package com.librechat.android.feature.chat.prompts +import androidx.compose.runtime.Immutable import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.librechat.android.core.common.result.Result import com.librechat.android.core.data.repository.PromptRepository import com.librechat.android.core.model.Prompt -import com.librechat.android.core.model.PromptGroup import com.librechat.android.core.model.request.AddPromptToGroupRequest import com.librechat.android.core.model.request.CreatePromptData import com.librechat.android.core.model.request.CreatePromptGroupData import com.librechat.android.core.model.request.CreatePromptRequest import com.librechat.android.core.model.request.UpdatePromptGroupRequest import com.librechat.android.core.model.request.UpdatePromptTagRequest -import androidx.compose.runtime.Immutable import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptVariablesSection.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptVariablesSection.kt index 08b9514..b7f7a5b 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptVariablesSection.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptVariablesSection.kt @@ -13,8 +13,8 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import com.librechat.android.feature.chat.R private val VARIABLE_REGEX = Regex("\\{\\{\\s*(\\w+)\\s*\\}\\}") diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptVersionsSheet.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptVersionsSheet.kt index 3c58280..d2d9ff4 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptVersionsSheet.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptVersionsSheet.kt @@ -22,10 +22,10 @@ import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.librechat.android.core.model.Prompt -import androidx.compose.ui.res.stringResource import com.librechat.android.feature.chat.R /** diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptsLibraryScreen.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptsLibraryScreen.kt index f3cac2d..e2cbb61 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptsLibraryScreen.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptsLibraryScreen.kt @@ -37,24 +37,24 @@ import androidx.compose.material3.Text import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import org.koin.compose.viewmodel.koinViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.librechat.android.core.ui.components.EmptyState import com.librechat.android.core.ui.components.ErrorBanner import com.librechat.android.core.ui.components.LibreChatTopBar import com.librechat.android.core.ui.components.LoadingIndicator +import com.librechat.android.feature.chat.R import com.librechat.android.feature.chat.prompts.components.PromptFilterSheet import com.librechat.android.feature.chat.prompts.components.PromptShareDialog import com.librechat.android.feature.chat.prompts.components.PromptSortOrder import com.librechat.android.feature.chat.prompts.components.VariableInputDialog -import androidx.compose.ui.res.stringResource -import com.librechat.android.feature.chat.R +import org.koin.compose.viewmodel.koinViewModel @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @Composable diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptsViewModel.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptsViewModel.kt index bfbb8ee..4800d3f 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptsViewModel.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptsViewModel.kt @@ -18,6 +18,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import timber.log.Timber @Immutable data class PromptsUiState( @@ -204,6 +205,7 @@ class PromptsViewModel( promptRepository.create(request) refresh() } catch (e: Exception) { + Timber.e(e, "Failed to create prompt") _uiState.value = _uiState.value.copy( error = "Failed to create prompt", ) @@ -222,6 +224,7 @@ class PromptsViewModel( promptRepository.update(groupId, request) refresh() } catch (e: Exception) { + Timber.e(e, "Failed to update prompt") _uiState.value = _uiState.value.copy( error = "Failed to update prompt", ) @@ -236,6 +239,7 @@ class PromptsViewModel( _uiState.value = _uiState.value.copy(selectedGroup = null) refresh() } catch (e: Exception) { + Timber.e(e, "Failed to delete prompt") _uiState.value = _uiState.value.copy( error = "Failed to delete prompt", ) diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/components/PromptFilterSheet.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/components/PromptFilterSheet.kt index 83bfc66..56bdfe5 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/components/PromptFilterSheet.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/components/PromptFilterSheet.kt @@ -18,8 +18,8 @@ import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import com.librechat.android.feature.chat.R enum class PromptSortOrder(val label: String) { diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/components/PromptPreviewPanel.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/components/PromptPreviewPanel.kt index b48b389..bc11c9f 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/components/PromptPreviewPanel.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/components/PromptPreviewPanel.kt @@ -9,8 +9,8 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import com.librechat.android.feature.chat.R @Composable diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/components/PromptShareDialog.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/components/PromptShareDialog.kt index 02e3196..e933bf4 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/components/PromptShareDialog.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/components/PromptShareDialog.kt @@ -28,8 +28,8 @@ 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.unit.dp import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import com.librechat.android.feature.chat.R enum class SharePermission(val label: String) { diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/components/VariableInputDialog.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/components/VariableInputDialog.kt index fad6361..7b18ace 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/components/VariableInputDialog.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/components/VariableInputDialog.kt @@ -26,8 +26,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import com.librechat.android.feature.chat.R private val VARIABLE_PATTERN = Regex("\\{\\{(\\w+)\\}\\}") diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/screen/ChatScreen.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/screen/ChatScreen.kt index 819e073..8525ffe 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/screen/ChatScreen.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/screen/ChatScreen.kt @@ -18,8 +18,8 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape @@ -53,34 +53,37 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.lifecycle.compose.collectAsStateWithLifecycle 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 com.librechat.android.core.model.ContentType -import com.librechat.android.core.model.MessageContentPart -import com.librechat.android.feature.chat.util.MessageNode import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import android.content.res.Configuration import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat -import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LifecycleEventEffect +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.librechat.android.core.common.EndpointConstants +import com.librechat.android.core.data.datastore.ChatFontSize +import com.librechat.android.core.data.datastore.LatexRenderer +import com.librechat.android.core.model.ContentType +import com.librechat.android.core.model.MessageContentPart import com.librechat.android.core.ui.components.LoadingIndicator +import com.librechat.android.core.ui.components.ModelParameterSheet +import com.librechat.android.core.ui.components.endpointIconRes +import com.librechat.android.core.ui.components.isMonochromeEndpointIcon +import com.librechat.android.feature.chat.R import com.librechat.android.feature.chat.components.ChatInput import com.librechat.android.feature.chat.components.ComparisonDualPane import com.librechat.android.feature.chat.components.ComparisonTabBar import com.librechat.android.feature.chat.components.ForkOptionsBottomSheet -import com.librechat.android.feature.chat.components.TempChatToggle import com.librechat.android.feature.chat.components.InConvoSearchBar import com.librechat.android.feature.chat.components.LandingContent import com.librechat.android.feature.chat.components.MessageList @@ -89,16 +92,12 @@ import com.librechat.android.feature.chat.components.ModelSelectorSheet import com.librechat.android.feature.chat.components.PresetPicker import com.librechat.android.feature.chat.components.SavePresetDialog import com.librechat.android.feature.chat.components.SecondaryMessageList -import com.librechat.android.core.ui.components.ModelParameterSheet -import com.librechat.android.core.data.datastore.ChatFontSize -import com.librechat.android.core.data.datastore.LatexRenderer -import com.librechat.android.core.ui.components.endpointIconRes -import com.librechat.android.core.ui.components.isMonochromeEndpointIcon +import com.librechat.android.feature.chat.components.TempChatToggle +import com.librechat.android.feature.chat.util.MessageNode import com.librechat.android.feature.chat.viewmodel.ChatScreenState import com.librechat.android.feature.chat.viewmodel.ChatViewModel import kotlinx.coroutines.launch -import androidx.compose.ui.res.stringResource -import com.librechat.android.feature.chat.R +import org.koin.compose.viewmodel.koinViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -422,8 +421,16 @@ fun ChatScreen( MessageList( displayMessages = primaryDisplayMessages, isStreaming = comparisonState.primaryIsStreaming || uiState.isStreaming, - streamingContent = if (comparisonState.primaryIsStreaming) comparisonState.primaryStreamingContent else uiState.streamingContent, - activeToolCalls = if (comparisonState.primaryIsStreaming) comparisonState.primaryActiveToolCalls else uiState.activeToolCalls, + streamingContent = if (comparisonState.primaryIsStreaming) { + comparisonState.primaryStreamingContent + } else { + uiState.streamingContent + }, + activeToolCalls = if (comparisonState.primaryIsStreaming) { + comparisonState.primaryActiveToolCalls + } else { + uiState.activeToolCalls + }, onSiblingNavigation = viewModel::switchBranch, onEditMessage = viewModel::startEditing, onRegenerateMessage = viewModel::regenerateMessage, @@ -760,6 +767,7 @@ private fun ChatTopBar( onLoadPreset: () -> Unit, onSavePreset: () -> Unit, onOpenDrawer: (() -> Unit)?, + modifier: Modifier = Modifier, onOpenSearch: () -> Unit = {}, onOpenPromptsLibrary: (() -> Unit)? = null, isTemporaryChat: Boolean = false, @@ -775,7 +783,6 @@ private fun ChatTopBar( onDuplicate: () -> Unit = {}, onArchive: () -> Unit = {}, onDelete: () -> Unit = {}, - modifier: Modifier = Modifier, ) { var showOverflowMenu by remember { mutableStateOf(false) } @@ -976,7 +983,6 @@ private fun ChatTopBar( } } } - } @Composable @@ -1116,4 +1122,3 @@ private fun buildComparisonDisplayMessages( } } } - diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/util/MessageTree.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/util/MessageNode.kt similarity index 100% rename from feature/chat/src/main/kotlin/com/librechat/android/feature/chat/util/MessageTree.kt rename to feature/chat/src/main/kotlin/com/librechat/android/feature/chat/util/MessageNode.kt diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/viewmodel/ChatViewModel.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/viewmodel/ChatViewModel.kt index d3f7429..ed01460 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/viewmodel/ChatViewModel.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/viewmodel/ChatViewModel.kt @@ -1,12 +1,10 @@ package com.librechat.android.feature.chat.viewmodel +import android.content.Context +import android.net.Uri import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import android.content.Context -import android.net.Uri -import timber.log.Timber -import com.librechat.android.feature.chat.util.NEW_CHAT_DRAFT_KEY import com.librechat.android.core.common.EndpointConstants import com.librechat.android.core.common.ToolConstants import com.librechat.android.core.common.network.ConnectivityObserver @@ -28,17 +26,17 @@ import com.librechat.android.core.data.repository.PromptRepository import com.librechat.android.core.data.repository.ShareRepository import com.librechat.android.core.data.repository.SpeechRepository import com.librechat.android.core.data.repository.UserRepository -import com.librechat.android.feature.chat.components.AttachedFile import com.librechat.android.core.model.EModelEndpoint -import com.librechat.android.core.model.request.AddedConversation -import com.librechat.android.core.model.request.EphemeralAgent import com.librechat.android.core.model.Preset import com.librechat.android.core.model.StreamEvent +import com.librechat.android.core.model.request.EphemeralAgent import com.librechat.android.core.ui.components.ModelParameters import com.librechat.android.feature.chat.PresetDisplayData import com.librechat.android.feature.chat.PromptMentionDisplayData -import com.librechat.android.feature.chat.util.buildActiveMessagePath import com.librechat.android.feature.chat.ShareIntentConsumer +import com.librechat.android.feature.chat.components.AttachedFile +import com.librechat.android.feature.chat.util.NEW_CHAT_DRAFT_KEY +import com.librechat.android.feature.chat.util.buildActiveMessagePath import com.librechat.android.feature.chat.viewmodel.delegate.ConversationActionsDelegate import com.librechat.android.feature.chat.viewmodel.delegate.FileAttachmentDelegate import com.librechat.android.feature.chat.viewmodel.delegate.InConversationSearchDelegate @@ -54,14 +52,13 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import timber.log.Timber import java.util.UUID class ChatViewModel( savedStateHandle: SavedStateHandle, @@ -90,12 +87,21 @@ class ChatViewModel( // --- Delegates --- private val searchDelegate = InConversationSearchDelegate(stateHandle) - private val conversationActionsDelegate = ConversationActionsDelegate(stateHandle, conversationRepository, shareRepository) - private val ttsDelegate = TextToSpeechDelegate(stateHandle, appContext, speechRepository, settingsDataStore, ::getMessageText) - private val voiceDelegate = VoiceInputDelegate(stateHandle, appContext, speechRepository, autoSendAfterStt = settingsDataStore.autoSendAfterStt.stateIn(viewModelScope, SharingStarted.Eagerly, false), onTranscriptionComplete = ::sendMessage) + private val conversationActionsDelegate = + ConversationActionsDelegate(stateHandle, conversationRepository, shareRepository) + private val ttsDelegate = + TextToSpeechDelegate(stateHandle, appContext, speechRepository, settingsDataStore, ::getMessageText) + private val voiceDelegate = + VoiceInputDelegate( + stateHandle, appContext, speechRepository, + autoSendAfterStt = settingsDataStore.autoSendAfterStt + .stateIn(viewModelScope, SharingStarted.Eagerly, false), + onTranscriptionComplete = ::sendMessage, + ) private val fileDelegate = FileAttachmentDelegate(stateHandle, appContext, fileRepository) private val presetPromptDelegate = PresetPromptDelegate(stateHandle, presetRepository, promptRepository) - private val modelDelegate = ModelSelectionDelegate(stateHandle, configRepository, agentRepository, mcpRepository, settingsDataStore, chatRepository) + private val modelDelegate = + ModelSelectionDelegate(stateHandle, configRepository, agentRepository, mcpRepository, settingsDataStore) // --- Delegate-owned flows exposed to the UI --- val attachedFiles: StateFlow> get() = fileDelegate.attachedFiles @@ -145,8 +151,10 @@ class ChatViewModel( private val streamingBuffer = StringBuilder() private var streamingBufferDirty = false private var wasStreaming = false + /** Tracks whether the last stream failure was a network error, to enable auto-reconnect. */ private var lastErrorWasNetwork = false + /** Job for the connectivity observer; started lazily only when a network error occurs. */ private var connectivityJob: Job? = null @@ -160,6 +168,7 @@ class ChatViewModel( /** True when this ViewModel was opened for a brand-new chat (no conversationId from navigation). */ private val isNewConversation: Boolean + /** Guard to ensure we only attempt title generation once per conversation. */ private var titleGenerationRequested = false @@ -277,7 +286,6 @@ class ChatViewModel( modelDelegate.loadAgents() loadSharedLinksEnabled() voiceDelegate.loadSpeechConfig() - } // ── Core chat flow ────────────────────────────────────────────── @@ -333,8 +341,16 @@ class ChatViewModel( model } _uiState.value = _uiState.value.copy( - selectedEndpoint = if (endpoint != null && (resolvedModel != null || isAgentConversation)) endpoint else _uiState.value.selectedEndpoint, - selectedModel = if (endpoint != null && resolvedModel != null) resolvedModel else _uiState.value.selectedModel, + selectedEndpoint = if (endpoint != null && (resolvedModel != null || isAgentConversation)) { + endpoint + } else { + _uiState.value.selectedEndpoint + }, + selectedModel = if (endpoint != null && resolvedModel != null) { + resolvedModel + } else { + _uiState.value.selectedModel + }, conversationTitle = conversation.title, ) } @@ -498,12 +514,18 @@ class ChatViewModel( val isAgent = _uiState.value.selectedEndpoint == EndpointConstants.AGENTS val webSearchEnabled = _uiState.value.modelParameters.webSearch val ephemeralAgent = buildEphemeralAgent() - Timber.d("sendMessage: webSearch=%s, endpoint=%s, model=%s, files=%d, ephemeralAgent=%s", webSearchEnabled, _uiState.value.selectedEndpoint, _uiState.value.selectedModel, fileRefs.size, ephemeralAgent) + Timber.d( + "sendMessage: webSearch=%s, endpoint=%s, model=%s, files=%d, ephemeralAgent=%s", + webSearchEnabled, _uiState.value.selectedEndpoint, + _uiState.value.selectedModel, fileRefs.size, ephemeralAgent, + ) // Build addedConvo if comparison mode is enabled val addedConvo = if (_uiState.value.comparisonState.isEnabled) { modelDelegate.buildAddedConvo(parentMessageId = lastMessageId) - } else null + } else { + null + } // Resolve effective endpoint/agentId for comparison mode. // All requests go through api/agents/chat/{endpoint} — the server's @@ -511,7 +533,7 @@ class ChatViewModel( // swapping is needed. Just keep the primary's original endpoint. val effectiveEndpoint = _uiState.value.selectedEndpoint val effectiveAgentId = if (isAgent) _uiState.value.selectedModel else null - val effectiveAddedConvo = addedConvo // null when no comparison + val effectiveAddedConvo = addedConvo // null when no comparison // If comparison enabled, prepare comparison streaming state if (addedConvo != null) { modelDelegate.primaryComparisonBuffer.clear() @@ -1154,6 +1176,7 @@ class ChatViewModel( loadConversation(conversationId) } } catch (e: Exception) { + Timber.e(e, "Could not resume stream") _uiState.value = _uiState.value.copy( isStreaming = false, streamingContent = "", diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/viewmodel/delegate/FileAttachmentDelegate.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/viewmodel/delegate/FileAttachmentDelegate.kt index c2940ed..61ce4d7 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/viewmodel/delegate/FileAttachmentDelegate.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/viewmodel/delegate/FileAttachmentDelegate.kt @@ -83,7 +83,8 @@ class FileAttachmentDelegate( val detectedMimeType = detectMimeTypeFromBytes(bytes) var mimeType = if (detectedMimeType != null && detectedMimeType != preliminaryMimeType) { Timber.w( - "uploadFile: MIME type mismatch for %s -- ContentResolver reported '%s' but actual content is '%s'. Using detected type.", + "uploadFile: MIME type mismatch for %s -- ContentResolver " + + "reported '%s' but actual content is '%s'. Using detected type.", filename, preliminaryMimeType, detectedMimeType, ) detectedMimeType @@ -210,12 +211,15 @@ class FileAttachmentDelegate( // Atomically mark failed and remove from list in one update _attachedFiles.update { currentList -> currentList.map { f -> - if (f.uri == uri) f.copy(uploadFailed = true, uploadProgress = null) - else f + if (f.uri == uri) { + f.copy(uploadFailed = true, uploadProgress = null) + } else { + f + } } } stateHandle.update { - copy(error = "Failed to upload ${filename}: ${result.message ?: "Unknown error"}") + copy(error = "Failed to upload $filename: ${result.message ?: "Unknown error"}") } } is Result.Loading -> { diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/viewmodel/delegate/ModelSelectionDelegate.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/viewmodel/delegate/ModelSelectionDelegate.kt index 94afc01..a9ccc65 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/viewmodel/delegate/ModelSelectionDelegate.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/viewmodel/delegate/ModelSelectionDelegate.kt @@ -5,7 +5,6 @@ import com.librechat.android.core.common.ToolConstants import com.librechat.android.core.common.result.Result import com.librechat.android.core.data.datastore.SettingsDataStore import com.librechat.android.core.data.repository.AgentRepository -import com.librechat.android.core.data.repository.ChatRepository import com.librechat.android.core.data.repository.ConfigRepository import com.librechat.android.core.data.repository.McpRepository import com.librechat.android.core.model.EModelEndpoint @@ -13,7 +12,6 @@ import com.librechat.android.core.model.mcp.McpServer import com.librechat.android.core.model.request.AddedConversation import com.librechat.android.core.ui.components.ModelParameters import com.librechat.android.feature.chat.McpServerDisplayData -import com.librechat.android.feature.chat.viewmodel.ActiveToolCall import com.librechat.android.feature.chat.viewmodel.ChatScreenState import com.librechat.android.feature.chat.viewmodel.ChatStateHandle import com.librechat.android.feature.chat.viewmodel.ComparisonState @@ -26,7 +24,6 @@ class ModelSelectionDelegate( private val agentRepository: AgentRepository, private val mcpRepository: McpRepository, private val settingsDataStore: SettingsDataStore, - private val chatRepository: ChatRepository, ) { // --- Model Comparison --- diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/viewmodel/delegate/PresetPromptDelegate.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/viewmodel/delegate/PresetPromptDelegate.kt index 588f165..af477fc 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/viewmodel/delegate/PresetPromptDelegate.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/viewmodel/delegate/PresetPromptDelegate.kt @@ -1,6 +1,5 @@ package com.librechat.android.feature.chat.viewmodel.delegate -import android.util.Log import com.librechat.android.core.common.result.Result import com.librechat.android.core.data.repository.PresetRepository import com.librechat.android.core.data.repository.PromptRepository @@ -11,6 +10,7 @@ import com.librechat.android.feature.chat.PresetDisplayData import com.librechat.android.feature.chat.PromptMentionDisplayData import com.librechat.android.feature.chat.viewmodel.ChatStateHandle import kotlinx.coroutines.launch +import timber.log.Timber class PresetPromptDelegate( private val stateHandle: ChatStateHandle, @@ -33,7 +33,7 @@ class PresetPromptDelegate( } is Result.Error -> { // Presets are non-critical; don't block the user - Log.d("PresetPromptDelegate", "Failed to load presets: ${result.message}", result.exception) + Timber.d(result.exception, "Failed to load presets: ${result.message}") } is Result.Loading -> { /* no-op */ } } @@ -51,7 +51,7 @@ class PresetPromptDelegate( } is Result.Error -> { // Prompts are non-critical; don't block the user - Log.d("PresetPromptDelegate", "Failed to load prompts: ${result.message}", result.exception) + Timber.d(result.exception, "Failed to load prompts: ${result.message}") } is Result.Loading -> { /* no-op */ } } @@ -74,6 +74,7 @@ class PresetPromptDelegate( presetRepository.create(preset) loadPresets() } catch (e: Exception) { + Timber.e(e, "Could not save preset") stateHandle.update { copy(error = "Could not save preset") } } } diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/viewmodel/delegate/TextToSpeechDelegate.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/viewmodel/delegate/TextToSpeechDelegate.kt index 65603f2..e7516aa 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/viewmodel/delegate/TextToSpeechDelegate.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/viewmodel/delegate/TextToSpeechDelegate.kt @@ -32,10 +32,11 @@ class TextToSpeechDelegate( ttsReady = status == TextToSpeech.SUCCESS if (ttsReady) { ttsEngine?.setOnUtteranceProgressListener(object : UtteranceProgressListener() { - override fun onStart(utteranceId: String?) {} + override fun onStart(utteranceId: String?) = Unit override fun onDone(utteranceId: String?) { stateHandle.update { copy(currentlyReadingMessageId = null) } } + @Deprecated("Deprecated in Java") override fun onError(utteranceId: String?) { stateHandle.update { @@ -76,7 +77,7 @@ class TextToSpeechDelegate( stateHandle.scope.launch { val source = settingsDataStore.ttsSource.first() if (source == "server") { - readAloudViaServer(messageId, text) + readAloudViaServer(text) } else { val rate = settingsDataStore.ttsSpeechRate.first() val pitch = settingsDataStore.ttsPitch.first() @@ -107,7 +108,7 @@ class TextToSpeechDelegate( } } - internal suspend fun readAloudViaServer(messageId: String, text: String) { + internal suspend fun readAloudViaServer(text: String) { when (val result = speechRepository.synthesizeSpeech(text)) { is Result.Success -> { try { @@ -192,7 +193,7 @@ class TextToSpeechDelegate( val source = settingsDataStore.ttsSource.first() if (source == "server") { - readAloudViaServer(syntheticId, responseText) + readAloudViaServer(responseText) } else { val rate = settingsDataStore.ttsSpeechRate.first() val pitch = settingsDataStore.ttsPitch.first() diff --git a/feature/conversations/build.gradle.kts b/feature/conversations/build.gradle.kts index 6e9a504..8eb3539 100644 --- a/feature/conversations/build.gradle.kts +++ b/feature/conversations/build.gradle.kts @@ -7,6 +7,7 @@ android { } dependencies { + implementation(libs.timber) implementation(libs.paging.runtime) implementation(libs.paging.compose) implementation(libs.kotlinx.serialization.json) diff --git a/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/components/ConversationActions.kt b/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/components/ConversationActions.kt index 81d6065..208c1ec 100644 --- a/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/components/ConversationActions.kt +++ b/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/components/ConversationActions.kt @@ -34,10 +34,10 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.librechat.android.core.model.Conversation import com.librechat.android.feature.conversations.R -import androidx.compose.ui.res.stringResource @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -47,9 +47,9 @@ fun ConversationActions( onRename: (String) -> Unit, onArchive: () -> Unit, onDelete: () -> Unit, + modifier: Modifier = Modifier, onTags: () -> Unit = {}, onShare: () -> Unit = {}, - onFork: () -> Unit = {}, onDuplicate: () -> Unit = {}, onExport: () -> Unit = {}, onBookmarkToggle: () -> Unit = {}, @@ -63,6 +63,7 @@ fun ConversationActions( ModalBottomSheet( onDismissRequest = onDismiss, + modifier = modifier, sheetState = sheetState, ) { Column( diff --git a/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/components/ConversationItem.kt b/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/components/ConversationItem.kt index e4c5d5c..326d198 100644 --- a/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/components/ConversationItem.kt +++ b/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/components/ConversationItem.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.librechat.android.core.common.extensions.toInstantOrNull @@ -30,12 +31,11 @@ import com.librechat.android.core.model.Conversation import com.librechat.android.core.model.EModelEndpoint import com.librechat.android.core.ui.components.isMonochromeIcon import com.librechat.android.core.ui.components.toIconRes +import com.librechat.android.feature.conversations.R import java.time.Instant import java.time.ZoneId import java.time.format.DateTimeFormatter import java.time.temporal.ChronoUnit -import com.librechat.android.feature.conversations.R -import androidx.compose.ui.res.stringResource /** * Lightweight snapshot of the fields ConversationItem actually renders. @@ -87,97 +87,97 @@ fun ConversationItem( .padding(horizontal = 16.dp, vertical = 12.dp), verticalAlignment = Alignment.CenterVertically, ) { - if (endpointIconRes != null) { - val isMonochrome = data.endpoint?.isMonochromeIcon() == true - Icon( - painter = painterResource(id = endpointIconRes), - contentDescription = endpointLabel, - modifier = Modifier.size(24.dp), - tint = if (isMonochrome) { - MaterialTheme.colorScheme.onSurfaceVariant - } else { - Color.Unspecified - }, - ) - } else { - Icon( - imageVector = Icons.Default.SmartToy, - contentDescription = endpointLabel, - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } + if (endpointIconRes != null) { + val isMonochrome = data.endpoint?.isMonochromeIcon() == true + Icon( + painter = painterResource(id = endpointIconRes), + contentDescription = endpointLabel, + modifier = Modifier.size(24.dp), + tint = if (isMonochrome) { + MaterialTheme.colorScheme.onSurfaceVariant + } else { + Color.Unspecified + }, + ) + } else { + Icon( + imageVector = Icons.Default.SmartToy, + contentDescription = endpointLabel, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } - Spacer(modifier = Modifier.width(12.dp)) + Spacer(modifier = Modifier.width(12.dp)) - Column( - modifier = Modifier.weight(1f), + Column( + modifier = Modifier.weight(1f), + ) { + Text( + text = data.title, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + + Row( + verticalAlignment = Alignment.CenterVertically, ) { Text( - text = data.title, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface, - maxLines = 1, - overflow = TextOverflow.Ellipsis, + text = endpointLabel, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, ) - Row( - verticalAlignment = Alignment.CenterVertically, - ) { + val modelName = data.model + if (modelName != null) { Text( - text = endpointLabel, + text = " \u00B7 ", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.outline, + ) + Text( + text = modelName, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f, fill = false), ) + } - val modelName = data.model - if (modelName != null) { - Text( - text = " \u00B7 ", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.outline, - ) - Text( - text = modelName, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.weight(1f, fill = false), - ) - } - - if (relativeTime.isNotEmpty()) { - Text( - text = " \u00B7 ", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.outline, - ) - Text( - text = relativeTime, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.outline, - ) - } + if (relativeTime.isNotEmpty()) { + Text( + text = " \u00B7 ", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.outline, + ) + Text( + text = relativeTime, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.outline, + ) } } + } - if (data.isBookmarked) { - Icon( - imageVector = Icons.Default.Bookmark, - contentDescription = stringResource(R.string.cd_bookmarked), - modifier = Modifier.size(16.dp), - tint = MaterialTheme.colorScheme.primary, - ) - } + if (data.isBookmarked) { + Icon( + imageVector = Icons.Default.Bookmark, + contentDescription = stringResource(R.string.cd_bookmarked), + modifier = Modifier.size(16.dp), + tint = MaterialTheme.colorScheme.primary, + ) + } - IconButton(onClick = onActionsClick) { - Icon( - imageVector = Icons.Default.MoreVert, - contentDescription = stringResource(R.string.cd_conversation_actions), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } + IconButton(onClick = onActionsClick) { + Icon( + imageVector = Icons.Default.MoreVert, + contentDescription = stringResource(R.string.cd_conversation_actions), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } } } diff --git a/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/components/ConversationSearchBar.kt b/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/components/ConversationSearchBar.kt index aa1cc34..1062f11 100644 --- a/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/components/ConversationSearchBar.kt +++ b/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/components/ConversationSearchBar.kt @@ -16,9 +16,9 @@ import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.librechat.android.feature.conversations.R -import androidx.compose.ui.res.stringResource @Composable fun ConversationSearchBar( diff --git a/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/components/TagFilterBar.kt b/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/components/TagFilterBar.kt index 9edddb2..a1e3476 100644 --- a/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/components/TagFilterBar.kt +++ b/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/components/TagFilterBar.kt @@ -26,10 +26,10 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.librechat.android.core.model.ConversationTag import com.librechat.android.feature.conversations.R -import androidx.compose.ui.res.stringResource @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/components/TagPicker.kt b/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/components/TagPicker.kt index d351e66..aa1ee6d 100644 --- a/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/components/TagPicker.kt +++ b/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/components/TagPicker.kt @@ -28,10 +28,10 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.librechat.android.core.model.ConversationTag import com.librechat.android.feature.conversations.R -import androidx.compose.ui.res.stringResource @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @Composable @@ -40,6 +40,7 @@ fun TagPicker( currentTags: List, onTagsChanged: (List) -> Unit, onDismiss: () -> Unit, + modifier: Modifier = Modifier, ) { val sheetState = rememberModalBottomSheetState() var selectedTags by remember { mutableStateOf(currentTags.toSet()) } @@ -50,6 +51,7 @@ fun TagPicker( onTagsChanged(selectedTags.toList()) onDismiss() }, + modifier = modifier, sheetState = sheetState, ) { Column( diff --git a/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/export/ExportFormatPicker.kt b/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/export/ExportFormatPicker.kt index c279354..cbb0c63 100644 --- a/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/export/ExportFormatPicker.kt +++ b/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/export/ExportFormatPicker.kt @@ -20,9 +20,9 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.librechat.android.feature.conversations.R -import androidx.compose.ui.res.stringResource enum class ExportFormat { JSON, @@ -34,11 +34,13 @@ enum class ExportFormat { fun ExportFormatPicker( onFormatSelected: (ExportFormat) -> Unit, onDismiss: () -> Unit, + modifier: Modifier = Modifier, ) { val sheetState = rememberModalBottomSheetState() ModalBottomSheet( onDismissRequest = onDismiss, + modifier = modifier, sheetState = sheetState, ) { Column( diff --git a/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/screen/ArchivedConversationsScreen.kt b/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/screen/ArchivedConversationsScreen.kt index 96b40d9..936f066 100644 --- a/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/screen/ArchivedConversationsScreen.kt +++ b/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/screen/ArchivedConversationsScreen.kt @@ -35,18 +35,18 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.librechat.android.feature.conversations.ArchivedConversationDisplayData import com.librechat.android.core.ui.components.EmptyState import com.librechat.android.core.ui.components.ErrorBanner import com.librechat.android.core.ui.components.LoadingIndicator +import com.librechat.android.feature.conversations.ArchivedConversationDisplayData +import com.librechat.android.feature.conversations.R import com.librechat.android.feature.conversations.viewmodel.ArchivedConversationsEvent import com.librechat.android.feature.conversations.viewmodel.ArchivedConversationsViewModel -import com.librechat.android.feature.conversations.R -import androidx.compose.ui.res.stringResource +import org.koin.compose.viewmodel.koinViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -146,7 +146,10 @@ fun ArchivedConversationsScreen( title = { Text(stringResource(R.string.delete_conversation)) }, text = { Text( - text = stringResource(R.string.delete_confirmation_message, deleteConfirmation?.title ?: stringResource(R.string.this_conversation)), + text = stringResource( + R.string.delete_confirmation_message, + deleteConfirmation?.title ?: stringResource(R.string.this_conversation), + ), style = MaterialTheme.typography.bodyMedium, ) }, diff --git a/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/screen/ConversationListScreen.kt b/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/screen/ConversationListScreen.kt index df3c5e9..2742537 100644 --- a/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/screen/ConversationListScreen.kt +++ b/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/screen/ConversationListScreen.kt @@ -42,13 +42,14 @@ import androidx.compose.runtime.snapshotFlow 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.unit.dp -import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.librechat.android.core.model.Conversation import com.librechat.android.core.ui.components.EmptyState import com.librechat.android.core.ui.components.ErrorBanner import com.librechat.android.core.ui.components.LoadingIndicator +import com.librechat.android.feature.conversations.R import com.librechat.android.feature.conversations.components.ConversationActions import com.librechat.android.feature.conversations.components.ConversationItem import com.librechat.android.feature.conversations.components.ConversationSearchBar @@ -59,8 +60,7 @@ import com.librechat.android.feature.conversations.export.ExportFormat import com.librechat.android.feature.conversations.export.ExportFormatPicker import com.librechat.android.feature.conversations.viewmodel.ConversationListEvent import com.librechat.android.feature.conversations.viewmodel.ConversationListViewModel -import com.librechat.android.feature.conversations.R -import androidx.compose.ui.res.stringResource +import org.koin.compose.viewmodel.koinViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -104,24 +104,6 @@ fun ConversationListScreen( pendingExportContent = null } - // SAF launcher for importing a JSON file - val importFileLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.OpenDocument(), - ) { uri -> - if (uri != null) { - try { - val jsonContent = context.contentResolver.openInputStream(uri)?.use { inputStream -> - inputStream.bufferedReader().readText() - } - if (jsonContent != null) { - viewModel.importConversation(jsonContent) - } - } catch (e: Exception) { - Toast.makeText(context, context.getString(R.string.import_failed, e.message ?: ""), Toast.LENGTH_SHORT).show() - } - } - } - // Collect one-shot events LaunchedEffect(Unit) { viewModel.events.collect { event -> @@ -379,11 +361,6 @@ fun ConversationListScreen( } selectedConversation = null }, - onFork = { - // Fork is a message-level action (requires a specific messageId). - // Use Duplicate for conversation-level copying. - selectedConversation = null - }, onDuplicate = { val convo = selectedConversation convo?.conversationId?.let { id -> diff --git a/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/viewmodel/ArchivedConversationsViewModel.kt b/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/viewmodel/ArchivedConversationsViewModel.kt index d951ef6..bb4889d 100644 --- a/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/viewmodel/ArchivedConversationsViewModel.kt +++ b/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/viewmodel/ArchivedConversationsViewModel.kt @@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import timber.log.Timber @Immutable data class ArchivedConversationsUiState( @@ -113,6 +114,7 @@ class ArchivedConversationsViewModel( try { conversationRepository.archive(id, false) } catch (e: Exception) { + Timber.e(e, "Failed to unarchive conversation") _events.emit(ArchivedConversationsEvent.ShowError("Failed to unarchive conversation")) } } @@ -123,6 +125,7 @@ class ArchivedConversationsViewModel( try { conversationRepository.delete(id) } catch (e: Exception) { + Timber.e(e, "Failed to delete conversation") _events.emit(ArchivedConversationsEvent.ShowError("Failed to delete conversation")) } } diff --git a/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/viewmodel/ConversationListViewModel.kt b/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/viewmodel/ConversationListViewModel.kt index eb0b272..ebd51ab 100644 --- a/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/viewmodel/ConversationListViewModel.kt +++ b/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/viewmodel/ConversationListViewModel.kt @@ -1,6 +1,5 @@ package com.librechat.android.feature.conversations.viewmodel -import android.util.Log import androidx.compose.runtime.Immutable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -28,6 +27,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import timber.log.Timber @Immutable data class ConversationListUiState( @@ -211,7 +211,7 @@ class ConversationListViewModel( } is Result.Error -> { // Tags are non-critical, silently fail - Log.d("ConversationListVM", "Failed to load tags: ${result.message}", result.exception) + Timber.d(result.exception, "Failed to load tags: ${result.message}") } is Result.Loading -> { /* no-op */ } } @@ -285,6 +285,7 @@ class ConversationListViewModel( try { conversationRepository.updateTitle(id, newTitle) } catch (e: Exception) { + Timber.e(e, "Failed to rename conversation") _events.emit(ConversationListEvent.ShowError("Failed to rename conversation")) } } @@ -295,6 +296,7 @@ class ConversationListViewModel( try { conversationRepository.archive(id, true) } catch (e: Exception) { + Timber.e(e, "Failed to archive conversation") _events.emit(ConversationListEvent.ShowError("Failed to archive conversation")) } } @@ -305,6 +307,7 @@ class ConversationListViewModel( try { conversationRepository.delete(id) } catch (e: Exception) { + Timber.e(e, "Failed to delete conversation") _events.emit(ConversationListEvent.ShowError("Failed to delete conversation")) } } diff --git a/feature/files/src/main/kotlin/com/librechat/android/feature/files/components/UploadProgressCard.kt b/feature/files/src/main/kotlin/com/librechat/android/feature/files/components/UploadProgressCard.kt index de3ff9a..ff1656c 100644 --- a/feature/files/src/main/kotlin/com/librechat/android/feature/files/components/UploadProgressCard.kt +++ b/feature/files/src/main/kotlin/com/librechat/android/feature/files/components/UploadProgressCard.kt @@ -25,10 +25,10 @@ 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.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.librechat.android.feature.files.R -import androidx.compose.ui.res.stringResource /** Animated card showing upload filename, progress bar, and cancel button with slide/fade visibility transitions. */ @Composable diff --git a/feature/files/src/main/kotlin/com/librechat/android/feature/files/di/FilesModule.kt b/feature/files/src/main/kotlin/com/librechat/android/feature/files/di/FilesModule.kt index 33d71e1..368ea44 100644 --- a/feature/files/src/main/kotlin/com/librechat/android/feature/files/di/FilesModule.kt +++ b/feature/files/src/main/kotlin/com/librechat/android/feature/files/di/FilesModule.kt @@ -1,7 +1,7 @@ package com.librechat.android.feature.files.di import com.librechat.android.feature.files.viewmodel.FilesViewModel -import org.koin.android.ext.koin.androidContext +import org.koin.android.ext.koin.androidApplication import org.koin.core.module.dsl.viewModel import org.koin.dsl.module @@ -9,7 +9,7 @@ val filesModule = module { viewModel { FilesViewModel( fileRepository = get(), - context = androidContext(), + context = androidApplication(), serverDataStore = get(), ) } diff --git a/feature/files/src/main/kotlin/com/librechat/android/feature/files/screen/FilePreviewDialog.kt b/feature/files/src/main/kotlin/com/librechat/android/feature/files/screen/FilePreviewDialog.kt index 15b6a11..9159edf 100644 --- a/feature/files/src/main/kotlin/com/librechat/android/feature/files/screen/FilePreviewDialog.kt +++ b/feature/files/src/main/kotlin/com/librechat/android/feature/files/screen/FilePreviewDialog.kt @@ -56,6 +56,7 @@ import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -64,19 +65,18 @@ import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import coil.compose.AsyncImage import coil.compose.AsyncImagePainter -import timber.log.Timber import com.librechat.android.feature.files.FilePreviewDisplayData -import java.io.File import com.librechat.android.feature.files.R -import androidx.compose.ui.res.stringResource +import timber.log.Timber +import java.io.File @OptIn(ExperimentalMaterial3Api::class) @Composable fun FilePreviewDialog( file: FilePreviewDisplayData, onDismiss: () -> Unit, - onDownloadFile: (suspend (fileId: String, userId: String?) -> ByteArray?)? = null, modifier: Modifier = Modifier, + onDownloadFile: (suspend (fileId: String, userId: String?) -> ByteArray?)? = null, ) { Dialog( onDismissRequest = onDismiss, diff --git a/feature/files/src/main/kotlin/com/librechat/android/feature/files/screen/FileSortMenu.kt b/feature/files/src/main/kotlin/com/librechat/android/feature/files/screen/FileSortMenu.kt index 7328f0e..e6b1d76 100644 --- a/feature/files/src/main/kotlin/com/librechat/android/feature/files/screen/FileSortMenu.kt +++ b/feature/files/src/main/kotlin/com/librechat/android/feature/files/screen/FileSortMenu.kt @@ -15,11 +15,11 @@ 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.stringResource import androidx.compose.ui.unit.dp +import com.librechat.android.feature.files.R import com.librechat.android.feature.files.viewmodel.FileSortField import com.librechat.android.feature.files.viewmodel.FileSortOrder -import com.librechat.android.feature.files.R -import androidx.compose.ui.res.stringResource @Composable fun FileSortMenu( diff --git a/feature/files/src/main/kotlin/com/librechat/android/feature/files/screen/FilesScreen.kt b/feature/files/src/main/kotlin/com/librechat/android/feature/files/screen/FilesScreen.kt index 4d7f9cb..6150311 100644 --- a/feature/files/src/main/kotlin/com/librechat/android/feature/files/screen/FilesScreen.kt +++ b/feature/files/src/main/kotlin/com/librechat/android/feature/files/screen/FilesScreen.kt @@ -23,7 +23,9 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.InsertDriveFile import androidx.compose.material.icons.automirrored.filled.Sort +import androidx.compose.material.icons.automirrored.filled.ViewList import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.AudioFile import androidx.compose.material.icons.filled.Close @@ -34,11 +36,9 @@ import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Folder import androidx.compose.material.icons.filled.GridView import androidx.compose.material.icons.filled.Image -import androidx.compose.material.icons.automirrored.filled.InsertDriveFile import androidx.compose.material.icons.filled.PictureAsPdf import androidx.compose.material.icons.filled.SelectAll import androidx.compose.material.icons.filled.VideoFile -import androidx.compose.material.icons.automirrored.filled.ViewList import androidx.compose.material3.AlertDialog import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults @@ -70,21 +70,21 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.compose.AsyncImage import com.librechat.android.core.ui.components.EmptyState import com.librechat.android.core.ui.components.ErrorBanner import com.librechat.android.feature.files.FileDisplayData +import com.librechat.android.feature.files.R import com.librechat.android.feature.files.components.UploadProgressCard import com.librechat.android.feature.files.viewmodel.FileTypeFilter import com.librechat.android.feature.files.viewmodel.FileViewMode import com.librechat.android.feature.files.viewmodel.FilesViewModel -import com.librechat.android.feature.files.R -import androidx.compose.ui.res.stringResource +import org.koin.compose.viewmodel.koinViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -121,8 +121,11 @@ fun FilesScreen( title = { val count = uiState.selectedFileIds.size Text( - if (count == 0) stringResource(R.string.select_files) - else stringResource(R.string.selected_count, count), + if (count == 0) { + stringResource(R.string.select_files) + } else { + stringResource(R.string.selected_count, count) + }, ) }, navigationIcon = { @@ -436,58 +439,58 @@ private fun FileItem( modifier: Modifier = Modifier, ) { Column(modifier = modifier.fillMaxWidth()) { - Row( - modifier = Modifier - .fillMaxWidth() - .combinedClickable( - onClick = onClick, - onLongClick = onLongClick, - ) - .padding(horizontal = 16.dp, vertical = 12.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - if (isEditMode) { - Checkbox( - checked = isSelected, - onCheckedChange = { onClick() }, - ) - Spacer(modifier = Modifier.width(8.dp)) - } + Row( + modifier = Modifier + .fillMaxWidth() + .combinedClickable( + onClick = onClick, + onLongClick = onLongClick, + ) + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + if (isEditMode) { + Checkbox( + checked = isSelected, + onCheckedChange = { onClick() }, + ) + Spacer(modifier = Modifier.width(8.dp)) + } - Icon( - imageVector = fileTypeIcon(file.type), - contentDescription = null, - modifier = Modifier.size(32.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - Spacer(modifier = Modifier.width(12.dp)) - Column(modifier = Modifier.weight(1f)) { - Text( - text = file.filename, - style = MaterialTheme.typography.bodyLarge, - maxLines = 1, - overflow = TextOverflow.Ellipsis, + Icon( + imageVector = fileTypeIcon(file.type), + contentDescription = null, + modifier = Modifier.size(32.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, ) - Text( - text = buildString { - append(file.formattedSize) - file.createdAt?.let { append(" \u00B7 $it") } - }, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - if (isEditMode) { - IconButton(onClick = onDelete) { - Icon( - imageVector = Icons.Default.Delete, - contentDescription = stringResource(R.string.cd_delete_file, file.filename), - tint = MaterialTheme.colorScheme.error, + Spacer(modifier = Modifier.width(12.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = file.filename, + style = MaterialTheme.typography.bodyLarge, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = buildString { + append(file.formattedSize) + file.createdAt?.let { append(" \u00B7 $it") } + }, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, ) } + if (isEditMode) { + IconButton(onClick = onDelete) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = stringResource(R.string.cd_delete_file, file.filename), + tint = MaterialTheme.colorScheme.error, + ) + } + } } - } - HorizontalDivider() + HorizontalDivider() } } diff --git a/feature/files/src/main/kotlin/com/librechat/android/feature/files/viewmodel/FilesViewModel.kt b/feature/files/src/main/kotlin/com/librechat/android/feature/files/viewmodel/FilesViewModel.kt index f5f86c7..a12c751 100644 --- a/feature/files/src/main/kotlin/com/librechat/android/feature/files/viewmodel/FilesViewModel.kt +++ b/feature/files/src/main/kotlin/com/librechat/android/feature/files/viewmodel/FilesViewModel.kt @@ -1,6 +1,6 @@ package com.librechat.android.feature.files.viewmodel -import android.content.Context +import android.app.Application import android.net.Uri import android.provider.OpenableColumns import android.text.format.Formatter @@ -69,7 +69,7 @@ data class FilesUiState( class FilesViewModel( private val fileRepository: FileRepository, - private val context: Context, + private val context: Application, private val serverDataStore: ServerDataStore, ) : ViewModel() { @@ -457,7 +457,7 @@ class FilesViewModel( filepath } else if (filepath.isNotBlank()) { // Relative path from server (e.g. /images/userId/file.webp) - "$baseUrl${filepath}" + "$baseUrl$filepath" } else { // Fallback: use the download endpoint "$baseUrl/api/files/download/${user ?: ""}/$fileId" @@ -501,7 +501,9 @@ class FilesViewModel( if (it.moveToFirst()) { val nameIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME) if (nameIndex >= 0) it.getString(nameIndex) else null - } else null + } else { + null + } } } } diff --git a/feature/files/src/test/kotlin/com/librechat/android/feature/files/viewmodel/FilesViewModelTest.kt b/feature/files/src/test/kotlin/com/librechat/android/feature/files/viewmodel/FilesViewModelTest.kt index 6f1a4e9..d9d12b3 100644 --- a/feature/files/src/test/kotlin/com/librechat/android/feature/files/viewmodel/FilesViewModelTest.kt +++ b/feature/files/src/test/kotlin/com/librechat/android/feature/files/viewmodel/FilesViewModelTest.kt @@ -1,6 +1,6 @@ package com.librechat.android.feature.files.viewmodel -import android.content.Context +import android.app.Application import android.text.format.Formatter import com.google.common.truth.Truth.assertThat import com.librechat.android.core.common.result.Result @@ -30,7 +30,7 @@ class FilesViewModelTest { private val testDispatcher = StandardTestDispatcher() private val fileRepository = mockk(relaxed = true) - private val context = mockk(relaxed = true) + private val context = mockk(relaxed = true) private val serverDataStore = mockk(relaxed = true) private lateinit var viewModel: FilesViewModel diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/di/SettingsModule.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/di/SettingsModule.kt index a01c732..78aa883 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/di/SettingsModule.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/di/SettingsModule.kt @@ -5,7 +5,7 @@ import com.librechat.android.feature.settings.viewmodel.McpViewModel import com.librechat.android.feature.settings.viewmodel.MemoriesViewModel import com.librechat.android.feature.settings.viewmodel.PresetManagerViewModel import com.librechat.android.feature.settings.viewmodel.SettingsViewModel -import org.koin.android.ext.koin.androidContext +import org.koin.android.ext.koin.androidApplication import org.koin.core.module.dsl.viewModel import org.koin.core.module.dsl.viewModelOf import org.koin.dsl.module @@ -13,7 +13,7 @@ import org.koin.dsl.module val settingsModule = module { viewModel { SettingsViewModel( - context = androidContext(), + context = androidApplication(), userRepository = get(), authRepository = get(), conversationRepository = get(), diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/navigation/SettingsNavigation.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/navigation/SettingsNavigation.kt index e241672..0af9902 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/navigation/SettingsNavigation.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/navigation/SettingsNavigation.kt @@ -1,6 +1,5 @@ package com.librechat.android.feature.settings.navigation -import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable @@ -14,6 +13,7 @@ import com.librechat.android.feature.settings.screen.PresetManagerScreen import com.librechat.android.feature.settings.screen.SharedLinksScreen import com.librechat.android.feature.settings.screen.TabbedSettingsScreen import com.librechat.android.feature.settings.viewmodel.SettingsViewModel +import org.koin.compose.viewmodel.koinViewModel const val SETTINGS_TABBED_ROUTE = "settings" const val SETTINGS_GENERAL_ROUTE = "settings/general" diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/AccountSettingsScreen.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/AccountSettingsScreen.kt index 81b9775..ee87f3f 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/AccountSettingsScreen.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/AccountSettingsScreen.kt @@ -21,7 +21,6 @@ import androidx.compose.material.icons.filled.Person import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import com.librechat.android.core.ui.components.OtpVerificationDialog import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon @@ -36,7 +35,6 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar - import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -48,21 +46,22 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.compose.AsyncImage -import com.librechat.android.feature.settings.R import com.librechat.android.core.ui.components.ErrorBanner import com.librechat.android.core.ui.components.LoadingIndicator +import com.librechat.android.core.ui.components.OtpVerificationDialog +import com.librechat.android.feature.settings.R import com.librechat.android.feature.settings.screen.sections.BackupCodesDialog import com.librechat.android.feature.settings.screen.sections.TwoFactorCodeDialog import com.librechat.android.feature.settings.screen.sections.TwoFactorSetupDialog import com.librechat.android.feature.settings.viewmodel.SettingsViewModel +import org.koin.compose.viewmodel.koinViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -139,225 +138,227 @@ fun AccountSettingsContent( } } - if (uiState.isLoading && uiState.user == null) { - LoadingIndicator() - } else if (uiState.error != null && uiState.user == null) { - ErrorBanner( - message = uiState.error ?: stringResource(R.string.error_could_not_load_settings), - modifier = modifier, - onRetry = { viewModel.retry() }, - ) - } else { - LazyColumn( - modifier = modifier.fillMaxSize(), - ) { - // Account section - item(key = "account_header") { - SectionHeader(stringResource(R.string.section_profile)) - } - item(key = "account_info") { - AccountInfo( - name = uiState.user?.name ?: "", - email = uiState.user?.email ?: "", - avatarUrl = uiState.user?.avatar, - onAvatarClick = viewModel::showAvatarDialog, - ) - } + Column { + if (uiState.isLoading && uiState.user == null) { + LoadingIndicator() + } else if (uiState.error != null && uiState.user == null) { + ErrorBanner( + message = uiState.error ?: stringResource(R.string.error_could_not_load_settings), + modifier = modifier, + onRetry = { viewModel.retry() }, + ) + } else { + LazyColumn( + modifier = modifier.fillMaxSize(), + ) { + // Account section + item(key = "account_header") { + SectionHeader(stringResource(R.string.section_profile)) + } + item(key = "account_info") { + AccountInfo( + name = uiState.user?.name ?: "", + email = uiState.user?.email ?: "", + avatarUrl = uiState.user?.avatar, + onAvatarClick = viewModel::showAvatarDialog, + ) + } - // Balance section - item(key = "balance_header") { - SectionHeader(stringResource(R.string.section_balance)) - } - item(key = "balance_section") { - BalanceSection( - tokenCredits = uiState.tokenCredits, - isLoading = uiState.isBalanceLoading, - ) - } + // Balance section + item(key = "balance_header") { + SectionHeader(stringResource(R.string.section_balance)) + } + item(key = "balance_section") { + BalanceSection( + tokenCredits = uiState.tokenCredits, + isLoading = uiState.isBalanceLoading, + ) + } - // Security section - item(key = "security_header") { - SectionHeader(stringResource(R.string.section_security)) - } - item(key = "security_settings") { - SecuritySection( - isTwoFactorEnabled = uiState.isTwoFactorEnabled, - isLoading = uiState.isTwoFactorLoading, - onToggleTwoFactor = viewModel::toggleTwoFactor, - onViewBackupCodes = viewModel::viewBackupCodes, - ) - } - item(key = "api_keys_row") { - AccountSettingsRow( - icon = Icons.Default.Key, - title = stringResource(R.string.api_keys), - subtitle = stringResource(R.string.api_keys_subtitle), - onClick = onNavigateToApiKeys, - ) - } + // Security section + item(key = "security_header") { + SectionHeader(stringResource(R.string.section_security)) + } + item(key = "security_settings") { + SecuritySection( + isTwoFactorEnabled = uiState.isTwoFactorEnabled, + isLoading = uiState.isTwoFactorLoading, + onToggleTwoFactor = viewModel::toggleTwoFactor, + onViewBackupCodes = viewModel::viewBackupCodes, + ) + } + item(key = "api_keys_row") { + AccountSettingsRow( + icon = Icons.Default.Key, + title = stringResource(R.string.api_keys), + subtitle = stringResource(R.string.api_keys_subtitle), + onClick = onNavigateToApiKeys, + ) + } - // Danger zone - item(key = "danger_header") { - SectionHeader(stringResource(R.string.section_danger_zone)) - } - item(key = "danger_actions") { - DangerZone( - isLoading = uiState.isLoading, - onLogoutClick = { showLogoutDialog = true }, - onDeleteClick = { showDeleteDialog = true }, - ) - } + // Danger zone + item(key = "danger_header") { + SectionHeader(stringResource(R.string.section_danger_zone)) + } + item(key = "danger_actions") { + DangerZone( + isLoading = uiState.isLoading, + onLogoutClick = { showLogoutDialog = true }, + onDeleteClick = { showDeleteDialog = true }, + ) + } - // Bottom spacing - item { Spacer(modifier = Modifier.height(32.dp)) } + // Bottom spacing + item { Spacer(modifier = Modifier.height(32.dp)) } + } } - } - // Avatar upload dialog - if (uiState.showAvatarDialog) { - AvatarUploadDialog( - currentAvatarUrl = uiState.user?.avatar, - isUploading = uiState.isAvatarUploading, - onPickImage = viewModel::uploadAvatar, - onDismiss = viewModel::dismissAvatarDialog, - ) - } + // Avatar upload dialog + if (uiState.showAvatarDialog) { + AvatarUploadDialog( + currentAvatarUrl = uiState.user?.avatar, + isUploading = uiState.isAvatarUploading, + onPickImage = viewModel::uploadAvatar, + onDismiss = viewModel::dismissAvatarDialog, + ) + } - // 2FA enable setup dialog - if (uiState.showTwoFactorSetupDialog) { - TwoFactorSetupDialog( - otpauthUrl = uiState.twoFactorOtpauthUrl, - isLoading = uiState.isTwoFactorLoading, - onConfirm = viewModel::confirmEnableTwoFactor, - onDismiss = viewModel::dismissTwoFactorSetupDialog, - ) - } + // 2FA enable setup dialog + if (uiState.showTwoFactorSetupDialog) { + TwoFactorSetupDialog( + otpauthUrl = uiState.twoFactorOtpauthUrl, + isLoading = uiState.isTwoFactorLoading, + onConfirm = viewModel::confirmEnableTwoFactor, + onDismiss = viewModel::dismissTwoFactorSetupDialog, + ) + } - // 2FA disable dialog - if (uiState.showDisableTwoFactorDialog) { - TwoFactorCodeDialog( - title = stringResource(R.string.dialog_title_disable_2fa), - description = stringResource(R.string.twofa_disable_instructions), - isLoading = uiState.isTwoFactorLoading, - onConfirm = viewModel::confirmDisableTwoFactor, - onDismiss = viewModel::dismissDisableTwoFactorDialog, - ) - } + // 2FA disable dialog + if (uiState.showDisableTwoFactorDialog) { + TwoFactorCodeDialog( + title = stringResource(R.string.dialog_title_disable_2fa), + description = stringResource(R.string.twofa_disable_instructions), + isLoading = uiState.isTwoFactorLoading, + onConfirm = viewModel::confirmDisableTwoFactor, + onDismiss = viewModel::dismissDisableTwoFactorDialog, + ) + } - // Backup codes dialog - if (uiState.showBackupCodesDialog) { - BackupCodesDialog( - backupCodes = uiState.backupCodes, - onDismiss = viewModel::dismissBackupCodesDialog, - ) - } + // Backup codes dialog + if (uiState.showBackupCodesDialog) { + BackupCodesDialog( + backupCodes = uiState.backupCodes, + onDismiss = viewModel::dismissBackupCodesDialog, + ) + } - // Logout confirmation dialog - if (showLogoutDialog) { - AlertDialog( - onDismissRequest = { showLogoutDialog = false }, - title = { Text(stringResource(R.string.dialog_title_sign_out)) }, - text = { Text(stringResource(R.string.dialog_sign_out_message)) }, - confirmButton = { - TextButton( - onClick = { - showLogoutDialog = false - viewModel.logout() - }, - ) { - Text(stringResource(R.string.action_sign_out)) - } - }, - dismissButton = { - TextButton(onClick = { showLogoutDialog = false }) { - Text(stringResource(R.string.action_cancel)) - } - }, - ) - } + // Logout confirmation dialog + if (showLogoutDialog) { + AlertDialog( + onDismissRequest = { showLogoutDialog = false }, + title = { Text(stringResource(R.string.dialog_title_sign_out)) }, + text = { Text(stringResource(R.string.dialog_sign_out_message)) }, + confirmButton = { + TextButton( + onClick = { + showLogoutDialog = false + viewModel.logout() + }, + ) { + Text(stringResource(R.string.action_sign_out)) + } + }, + dismissButton = { + TextButton(onClick = { showLogoutDialog = false }) { + Text(stringResource(R.string.action_cancel)) + } + }, + ) + } - // Delete account confirmation dialog - if (showDeleteDialog) { - AlertDialog( - onDismissRequest = { showDeleteDialog = false }, - title = { Text(stringResource(R.string.dialog_title_delete_account)) }, - text = { - Text(stringResource(R.string.dialog_delete_account_message)) - }, - confirmButton = { - TextButton( - onClick = { - showDeleteDialog = false - viewModel.deleteAccount() - }, - colors = ButtonDefaults.textButtonColors( - contentColor = MaterialTheme.colorScheme.error, - ), - ) { - Text(stringResource(R.string.action_delete)) - } - }, - dismissButton = { - TextButton(onClick = { showDeleteDialog = false }) { - Text(stringResource(R.string.action_cancel)) - } - }, - ) - } + // Delete account confirmation dialog + if (showDeleteDialog) { + AlertDialog( + onDismissRequest = { showDeleteDialog = false }, + title = { Text(stringResource(R.string.dialog_title_delete_account)) }, + text = { + Text(stringResource(R.string.dialog_delete_account_message)) + }, + confirmButton = { + TextButton( + onClick = { + showDeleteDialog = false + viewModel.deleteAccount() + }, + colors = ButtonDefaults.textButtonColors( + contentColor = MaterialTheme.colorScheme.error, + ), + ) { + Text(stringResource(R.string.action_delete)) + } + }, + dismissButton = { + TextButton(onClick = { showDeleteDialog = false }) { + Text(stringResource(R.string.action_cancel)) + } + }, + ) + } - // OTP dialog for account deletion when 2FA is enabled - if (uiState.showDeleteAccountOtpDialog) { - OtpVerificationDialog( - title = stringResource(R.string.otp_title_verify_identity), - description = stringResource(R.string.otp_desc_delete_account), - isLoading = uiState.isLoading, - onVerify = { token, backupCode -> - viewModel.deleteAccount(token = token, backupCode = backupCode) - }, - onDismiss = viewModel::dismissDeleteAccountOtpDialog, - verifyLabel = stringResource(R.string.otp_verify), - cancelLabel = stringResource(R.string.otp_cancel), - backupCodeLabel = stringResource(R.string.otp_backup_code_label), - useBackupToggleLabel = stringResource(R.string.otp_use_backup_code), - useOtpToggleLabel = stringResource(R.string.otp_use_otp_code), - ) - } + // OTP dialog for account deletion when 2FA is enabled + if (uiState.showDeleteAccountOtpDialog) { + OtpVerificationDialog( + title = stringResource(R.string.otp_title_verify_identity), + description = stringResource(R.string.otp_desc_delete_account), + isLoading = uiState.isLoading, + onVerify = { token, backupCode -> + viewModel.deleteAccount(token = token, backupCode = backupCode) + }, + onDismiss = viewModel::dismissDeleteAccountOtpDialog, + verifyLabel = stringResource(R.string.otp_verify), + cancelLabel = stringResource(R.string.otp_cancel), + backupCodeLabel = stringResource(R.string.otp_backup_code_label), + useBackupToggleLabel = stringResource(R.string.otp_use_backup_code), + useOtpToggleLabel = stringResource(R.string.otp_use_otp_code), + ) + } - // OTP dialog for enabling 2FA when re-enrolling - if (uiState.showEnableTwoFactorOtpDialog) { - OtpVerificationDialog( - title = stringResource(R.string.otp_title_verify_identity), - description = stringResource(R.string.otp_desc_reenroll_2fa), - isLoading = uiState.isTwoFactorLoading, - onVerify = { token, backupCode -> - viewModel.enableTwoFactorWithOtp(token = token, backupCode = backupCode) - }, - onDismiss = viewModel::dismissEnableTwoFactorOtpDialog, - verifyLabel = stringResource(R.string.otp_verify), - cancelLabel = stringResource(R.string.otp_cancel), - backupCodeLabel = stringResource(R.string.otp_backup_code_label), - useBackupToggleLabel = stringResource(R.string.otp_use_backup_code), - useOtpToggleLabel = stringResource(R.string.otp_use_otp_code), - ) - } + // OTP dialog for enabling 2FA when re-enrolling + if (uiState.showEnableTwoFactorOtpDialog) { + OtpVerificationDialog( + title = stringResource(R.string.otp_title_verify_identity), + description = stringResource(R.string.otp_desc_reenroll_2fa), + isLoading = uiState.isTwoFactorLoading, + onVerify = { token, backupCode -> + viewModel.enableTwoFactorWithOtp(token = token, backupCode = backupCode) + }, + onDismiss = viewModel::dismissEnableTwoFactorOtpDialog, + verifyLabel = stringResource(R.string.otp_verify), + cancelLabel = stringResource(R.string.otp_cancel), + backupCodeLabel = stringResource(R.string.otp_backup_code_label), + useBackupToggleLabel = stringResource(R.string.otp_use_backup_code), + useOtpToggleLabel = stringResource(R.string.otp_use_otp_code), + ) + } - // OTP dialog for regenerating backup codes - if (uiState.showBackupCodesOtpDialog) { - OtpVerificationDialog( - title = stringResource(R.string.otp_title_verify_identity), - description = stringResource(R.string.otp_desc_regenerate_backup_codes), - isLoading = uiState.isTwoFactorLoading, - onVerify = { token, backupCode -> - viewModel.viewBackupCodesWithOtp(token = token, backupCode = backupCode) - }, - onDismiss = viewModel::dismissBackupCodesOtpDialog, - verifyLabel = stringResource(R.string.otp_verify), - cancelLabel = stringResource(R.string.otp_cancel), - backupCodeLabel = stringResource(R.string.otp_backup_code_label), - useBackupToggleLabel = stringResource(R.string.otp_use_backup_code), - useOtpToggleLabel = stringResource(R.string.otp_use_otp_code), - ) - } + // OTP dialog for regenerating backup codes + if (uiState.showBackupCodesOtpDialog) { + OtpVerificationDialog( + title = stringResource(R.string.otp_title_verify_identity), + description = stringResource(R.string.otp_desc_regenerate_backup_codes), + isLoading = uiState.isTwoFactorLoading, + onVerify = { token, backupCode -> + viewModel.viewBackupCodesWithOtp(token = token, backupCode = backupCode) + }, + onDismiss = viewModel::dismissBackupCodesOtpDialog, + verifyLabel = stringResource(R.string.otp_verify), + cancelLabel = stringResource(R.string.otp_cancel), + backupCodeLabel = stringResource(R.string.otp_backup_code_label), + useBackupToggleLabel = stringResource(R.string.otp_use_backup_code), + useOtpToggleLabel = stringResource(R.string.otp_use_otp_code), + ) + } + } // Column } @Composable @@ -379,64 +380,66 @@ private fun AccountInfo( avatarUrl: String? = null, onAvatarClick: () -> Unit = {}, ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Surface( + Column { + Row( modifier = Modifier - .size(48.dp) - .clip(CircleShape), - color = MaterialTheme.colorScheme.primaryContainer, - onClick = onAvatarClick, + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, ) { - if (avatarUrl != null) { - AsyncImage( - model = avatarUrl, - contentDescription = stringResource(R.string.cd_user_avatar), - modifier = Modifier.size(48.dp), - contentScale = ContentScale.Crop, - ) - } else { + Surface( + modifier = Modifier + .size(48.dp) + .clip(CircleShape), + color = MaterialTheme.colorScheme.primaryContainer, + onClick = onAvatarClick, + ) { + if (avatarUrl != null) { + AsyncImage( + model = avatarUrl, + contentDescription = stringResource(R.string.cd_user_avatar), + modifier = Modifier.size(48.dp), + contentScale = ContentScale.Crop, + ) + } else { + Icon( + imageVector = Icons.Default.Person, + contentDescription = null, + modifier = Modifier.padding(12.dp), + tint = MaterialTheme.colorScheme.onPrimaryContainer, + ) + } + } + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + if (name.isNotBlank()) { + Text( + text = name, + style = MaterialTheme.typography.bodyLarge, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + if (email.isNotBlank()) { + Text( + text = email, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + IconButton(onClick = onAvatarClick) { Icon( - imageVector = Icons.Default.Person, - contentDescription = null, - modifier = Modifier.padding(12.dp), - tint = MaterialTheme.colorScheme.onPrimaryContainer, + imageVector = Icons.Default.CameraAlt, + contentDescription = stringResource(R.string.cd_change_avatar), + modifier = Modifier.size(20.dp), ) } } - Spacer(modifier = Modifier.width(16.dp)) - Column(modifier = Modifier.weight(1f)) { - if (name.isNotBlank()) { - Text( - text = name, - style = MaterialTheme.typography.bodyLarge, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - if (email.isNotBlank()) { - Text( - text = email, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - } - IconButton(onClick = onAvatarClick) { - Icon( - imageVector = Icons.Default.CameraAlt, - contentDescription = stringResource(R.string.cd_change_avatar), - modifier = Modifier.size(20.dp), - ) - } - } - HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) + HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) + } // Column } @Composable @@ -518,4 +521,3 @@ private fun AccountSettingsRow( } HorizontalDivider() } - diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/ApiKeyCreateDialog.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/ApiKeyCreateDialog.kt index 522f6d9..5a3c23b 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/ApiKeyCreateDialog.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/ApiKeyCreateDialog.kt @@ -26,12 +26,12 @@ 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.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import com.librechat.android.feature.settings.R import com.librechat.android.core.model.ApiKey +import com.librechat.android.feature.settings.R /** * Two-phase API key creation dialog. diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/ApiKeysScreen.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/ApiKeysScreen.kt index f0faca3..0768e2b 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/ApiKeysScreen.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/ApiKeysScreen.kt @@ -42,14 +42,14 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.librechat.android.feature.settings.R import com.librechat.android.core.model.ApiKey +import com.librechat.android.feature.settings.R import com.librechat.android.feature.settings.viewmodel.ApiKeysViewModel +import org.koin.compose.viewmodel.koinViewModel /** Lists user API keys with create (FAB), delete, and pull-to-refresh. */ @OptIn(ExperimentalMaterial3Api::class) diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/AvatarUploadDialog.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/AvatarUploadDialog.kt index 169ec6b..fd93ec2 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/AvatarUploadDialog.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/AvatarUploadDialog.kt @@ -30,12 +30,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.res.stringResource -import com.librechat.android.feature.settings.R import androidx.compose.ui.unit.dp import coil.compose.AsyncImage +import com.librechat.android.feature.settings.R @Composable internal fun AvatarUploadDialog( diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/BalanceSection.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/BalanceSection.kt index 832bfaf..6419652 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/BalanceSection.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/BalanceSection.kt @@ -20,9 +20,9 @@ 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.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.librechat.android.feature.settings.R import java.text.NumberFormat @@ -37,64 +37,66 @@ internal fun BalanceSection( R.string.cd_token_balance, NumberFormat.getNumberInstance().format(tokenCredits), ) - Column( - modifier = modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - ) { - Card( + Column(modifier = modifier) { + Column( modifier = Modifier .fillMaxWidth() - .semantics { - contentDescription = balanceCd - }, - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.4f), - ), + .padding(horizontal = 16.dp, vertical = 8.dp), ) { - Row( + Card( modifier = Modifier .fillMaxWidth() - .padding(16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, + .semantics { + contentDescription = balanceCd + }, + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.4f), + ), ) { Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, ) { - Icon( - imageVector = Icons.Default.AccountBalanceWallet, - contentDescription = null, - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.primary, - ) - Spacer(modifier = Modifier.width(12.dp)) - Column { - Text( - text = stringResource(R.string.token_credits), - style = MaterialTheme.typography.bodyMedium, + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Default.AccountBalanceWallet, + contentDescription = null, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.primary, ) + Spacer(modifier = Modifier.width(12.dp)) + Column { + Text( + text = stringResource(R.string.token_credits), + style = MaterialTheme.typography.bodyMedium, + ) + Text( + text = stringResource(R.string.available_balance), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(20.dp), + strokeWidth = 2.dp, + ) + } else { Text( - text = stringResource(R.string.available_balance), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, + text = NumberFormat.getNumberInstance().format(tokenCredits), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary, ) } } - if (isLoading) { - CircularProgressIndicator( - modifier = Modifier.size(20.dp), - strokeWidth = 2.dp, - ) - } else { - Text( - text = NumberFormat.getNumberInstance().format(tokenCredits), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.primary, - ) - } } } + HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) } - HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) } diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/ChatSettingsScreen.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/ChatSettingsScreen.kt index fb8a1c5..3011b4e 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/ChatSettingsScreen.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/ChatSettingsScreen.kt @@ -32,14 +32,14 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.librechat.android.feature.settings.R import com.librechat.android.feature.settings.viewmodel.SettingsViewModel +import org.koin.compose.viewmodel.koinViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -87,159 +87,161 @@ fun ChatSettingsContent( ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() - LazyColumn( - modifier = modifier.fillMaxSize(), - ) { - // Chat Preferences section - item(key = "chat_header") { - SectionHeader(stringResource(R.string.section_chat_preferences)) - } - item(key = "chat_settings") { - ChatSettingsSection( - fontSize = uiState.chatFontSize, - autoScrollEnabled = uiState.autoScrollEnabled, - showThinkingBlocks = uiState.showThinkingBlocks, - showImageDescriptions = uiState.showImageDescriptions, - dismissKeyboardOnSend = uiState.dismissKeyboardOnSend, - chatLayoutStyle = uiState.chatLayoutStyle, - showAvatars = uiState.showAvatars, - showBubbles = uiState.showBubbles, - latexRenderer = uiState.latexRenderer, - onFontSizeChange = viewModel::setChatFontSize, - onAutoScrollChange = viewModel::setAutoScrollEnabled, - onShowThinkingChange = viewModel::setShowThinkingBlocks, - onShowImageDescriptionsChange = viewModel::setShowImageDescriptions, - onDismissKeyboardOnSendChange = viewModel::setDismissKeyboardOnSend, - onChatLayoutStyleChange = viewModel::setChatLayoutStyle, - onShowAvatarsChange = viewModel::setShowAvatars, - onShowBubblesChange = viewModel::setShowBubbles, - onLatexRendererChange = viewModel::setLatexRenderer, - ) - } - - // Presets section - item(key = "presets_header") { - SectionHeader(stringResource(R.string.section_presets)) - } - item(key = "presets_row") { - ChatSettingsRow( - icon = Icons.Default.Brush, - title = stringResource(R.string.presets), - subtitle = stringResource(R.string.presets_subtitle), - onClick = onNavigateToPresets, - ) - } - - // Advanced section - item(key = "advanced_header") { - SectionHeader(stringResource(R.string.section_advanced)) - } - item(key = "fork_settings_row") { - ChatSettingsRow( - icon = Icons.AutoMirrored.Filled.Chat, - title = stringResource(R.string.fork_behavior), - subtitle = ForkMode.fromApiValue(uiState.forkMode).label, - onClick = viewModel::showForkSettingsDialog, - ) - } - item(key = "commands_row") { - ChatSettingsRow( - icon = Icons.Default.Terminal, - title = stringResource(R.string.commands), - subtitle = stringResource(R.string.commands_enabled_count, uiState.commands.count { it.enabled }), - onClick = viewModel::showCommandsScreen, - ) - } - - // Speech section - item(key = "speech_header") { - SectionHeader(stringResource(R.string.section_speech)) - } - item(key = "speech_settings") { - SpeechSettingsSection( - autoSendAfterSttEnabled = uiState.sttAutoSend, - autoReadEnabled = uiState.autoReadEnabled, - selectedVoice = uiState.selectedVoice, - availableVoices = uiState.availableVoices, - ttsSource = uiState.ttsSource, - onAutoSendAfterSttChange = viewModel::setAutoSendAfterStt, - onAutoReadChange = viewModel::setAutoReadEnabled, - onVoiceSelected = viewModel::selectVoice, - onTestVoice = viewModel::testVoice, - ) - } - item(key = "speech_detail_buttons") { - SpeechDetailButtons( - onSttDetailClick = viewModel::showSttDetailDialog, - onTtsDetailClick = viewModel::showTtsDetailDialog, - ) - } - - // Bottom spacing - item { Spacer(modifier = Modifier.height(32.dp)) } - } - - // Fork settings dialog - if (uiState.showForkSettingsDialog) { - ForkSettingsDialog( - selectedMode = ForkMode.fromApiValue(uiState.forkMode), - onModeSelected = { mode -> - viewModel.setForkMode(mode.apiValue) - }, - onDismiss = viewModel::dismissForkSettingsDialog, - ) - } - - // STT detail dialog - if (uiState.showSttDetailDialog) { - SttDetailDialog( - selectedEngine = uiState.sttEngine, - selectedLanguage = uiState.sttLanguage, - availableEngines = listOf("Default", "Whisper", "Google"), - availableLanguages = listOf("Auto-detect", "English", "Spanish", "French", "German", "Japanese", "Chinese"), - onConfirm = viewModel::saveSttSettings, - onDismiss = viewModel::dismissSttDetailDialog, - ) - } - - // TTS detail dialog - if (uiState.showTtsDetailDialog) { - TtsDetailDialog( - selectedEngine = uiState.ttsEngine, - selectedVoice = uiState.ttsVoice, - speechRate = uiState.ttsSpeechRate, - pitch = uiState.ttsPitch, - deviceVoiceName = uiState.ttsDeviceVoiceName, - cachingEnabled = uiState.ttsCaching, - ttsSource = uiState.ttsSource, - availableEngines = listOf("Default", "ElevenLabs", "OpenAI"), - availableVoices = uiState.availableVoices.map { it.name }.ifEmpty { - listOf("Default", "Alloy", "Echo", "Fable", "Onyx", "Nova", "Shimmer") - }, - availableDeviceVoices = uiState.availableDeviceVoices, - isPreviewPlaying = uiState.isTtsPreviewPlaying, - onPreviewDevice = viewModel::previewDeviceTts, - onPreviewServer = viewModel::previewServerTts, - onStopPreview = viewModel::stopTtsPreview, - onConfirm = viewModel::saveTtsSettings, - onDismiss = viewModel::dismissTtsDetailDialog, - ) - } - - // Commands screen (full screen overlay) - if (uiState.showCommandsScreen) { - CommandsConfigScreen( - commands = uiState.commands.map { cmd -> - CommandConfig( - name = cmd.name, - description = cmd.description, - enabled = cmd.enabled, + Column { + LazyColumn( + modifier = modifier.fillMaxSize(), + ) { + // Chat Preferences section + item(key = "chat_header") { + SectionHeader(stringResource(R.string.section_chat_preferences)) + } + item(key = "chat_settings") { + ChatSettingsSection( + fontSize = uiState.chatFontSize, + autoScrollEnabled = uiState.autoScrollEnabled, + showThinkingBlocks = uiState.showThinkingBlocks, + showImageDescriptions = uiState.showImageDescriptions, + dismissKeyboardOnSend = uiState.dismissKeyboardOnSend, + chatLayoutStyle = uiState.chatLayoutStyle, + showAvatars = uiState.showAvatars, + showBubbles = uiState.showBubbles, + latexRenderer = uiState.latexRenderer, + onFontSizeChange = viewModel::setChatFontSize, + onAutoScrollChange = viewModel::setAutoScrollEnabled, + onShowThinkingChange = viewModel::setShowThinkingBlocks, + onShowImageDescriptionsChange = viewModel::setShowImageDescriptions, + onDismissKeyboardOnSendChange = viewModel::setDismissKeyboardOnSend, + onChatLayoutStyleChange = viewModel::setChatLayoutStyle, + onShowAvatarsChange = viewModel::setShowAvatars, + onShowBubblesChange = viewModel::setShowBubbles, + onLatexRendererChange = viewModel::setLatexRenderer, ) - }, - onToggleCommand = viewModel::toggleCommand, - onNavigateBack = viewModel::hideCommandsScreen, - ) - } + } + + // Presets section + item(key = "presets_header") { + SectionHeader(stringResource(R.string.section_presets)) + } + item(key = "presets_row") { + ChatSettingsRow( + icon = Icons.Default.Brush, + title = stringResource(R.string.presets), + subtitle = stringResource(R.string.presets_subtitle), + onClick = onNavigateToPresets, + ) + } + + // Advanced section + item(key = "advanced_header") { + SectionHeader(stringResource(R.string.section_advanced)) + } + item(key = "fork_settings_row") { + ChatSettingsRow( + icon = Icons.AutoMirrored.Filled.Chat, + title = stringResource(R.string.fork_behavior), + subtitle = ForkMode.fromApiValue(uiState.forkMode).label, + onClick = viewModel::showForkSettingsDialog, + ) + } + item(key = "commands_row") { + ChatSettingsRow( + icon = Icons.Default.Terminal, + title = stringResource(R.string.commands), + subtitle = stringResource(R.string.commands_enabled_count, uiState.commands.count { it.enabled }), + onClick = viewModel::showCommandsScreen, + ) + } + + // Speech section + item(key = "speech_header") { + SectionHeader(stringResource(R.string.section_speech)) + } + item(key = "speech_settings") { + SpeechSettingsSection( + autoSendAfterSttEnabled = uiState.sttAutoSend, + autoReadEnabled = uiState.autoReadEnabled, + selectedVoice = uiState.selectedVoice, + availableVoices = uiState.availableVoices, + ttsSource = uiState.ttsSource, + onAutoSendAfterSttChange = viewModel::setAutoSendAfterStt, + onAutoReadChange = viewModel::setAutoReadEnabled, + onVoiceSelected = viewModel::selectVoice, + onTestVoice = viewModel::testVoice, + ) + } + item(key = "speech_detail_buttons") { + SpeechDetailButtons( + onSttDetailClick = viewModel::showSttDetailDialog, + onTtsDetailClick = viewModel::showTtsDetailDialog, + ) + } + + // Bottom spacing + item { Spacer(modifier = Modifier.height(32.dp)) } + } + + // Fork settings dialog + if (uiState.showForkSettingsDialog) { + ForkSettingsDialog( + selectedMode = ForkMode.fromApiValue(uiState.forkMode), + onModeSelected = { mode -> + viewModel.setForkMode(mode.apiValue) + }, + onDismiss = viewModel::dismissForkSettingsDialog, + ) + } + + // STT detail dialog + if (uiState.showSttDetailDialog) { + SttDetailDialog( + selectedEngine = uiState.sttEngine, + selectedLanguage = uiState.sttLanguage, + availableEngines = listOf("Default", "Whisper", "Google"), + availableLanguages = listOf("Auto-detect", "English", "Spanish", "French", "German", "Japanese", "Chinese"), + onConfirm = viewModel::saveSttSettings, + onDismiss = viewModel::dismissSttDetailDialog, + ) + } + + // TTS detail dialog + if (uiState.showTtsDetailDialog) { + TtsDetailDialog( + selectedEngine = uiState.ttsEngine, + selectedVoice = uiState.ttsVoice, + speechRate = uiState.ttsSpeechRate, + pitch = uiState.ttsPitch, + deviceVoiceName = uiState.ttsDeviceVoiceName, + cachingEnabled = uiState.ttsCaching, + ttsSource = uiState.ttsSource, + availableEngines = listOf("Default", "ElevenLabs", "OpenAI"), + availableVoices = uiState.availableVoices.map { it.name }.ifEmpty { + listOf("Default", "Alloy", "Echo", "Fable", "Onyx", "Nova", "Shimmer") + }, + availableDeviceVoices = uiState.availableDeviceVoices, + isPreviewPlaying = uiState.isTtsPreviewPlaying, + onPreviewDevice = viewModel::previewDeviceTts, + onPreviewServer = viewModel::previewServerTts, + onStopPreview = viewModel::stopTtsPreview, + onConfirm = viewModel::saveTtsSettings, + onDismiss = viewModel::dismissTtsDetailDialog, + ) + } + + // Commands screen (full screen overlay) + if (uiState.showCommandsScreen) { + CommandsConfigScreen( + commands = uiState.commands.map { cmd -> + CommandConfig( + name = cmd.name, + description = cmd.description, + enabled = cmd.enabled, + ) + }, + onToggleCommand = viewModel::toggleCommand, + onNavigateBack = viewModel::hideCommandsScreen, + ) + } + } // Column } @Composable @@ -259,48 +261,50 @@ private fun SpeechDetailButtons( onSttDetailClick: () -> Unit, onTtsDetailClick: () -> Unit, ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 4.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - OutlinedButton( - onClick = onSttDetailClick, - modifier = Modifier.fillMaxWidth(), + Column { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 4.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), ) { - Row( + OutlinedButton( + onClick = onSttDetailClick, modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, ) { - Text(stringResource(R.string.speech_to_text_settings)) - Icon( - imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - } - } - OutlinedButton( - onClick = onTtsDetailClick, - modifier = Modifier.fillMaxWidth(), - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text(stringResource(R.string.text_to_speech_settings)) - Icon( - imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text(stringResource(R.string.speech_to_text_settings)) + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + } + } + OutlinedButton( + onClick = onTtsDetailClick, + modifier = Modifier.fillMaxWidth(), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text(stringResource(R.string.text_to_speech_settings)) + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + } } } + HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) } - HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) } @Composable @@ -311,41 +315,43 @@ private fun ChatSettingsRow( onClick: () -> Unit, modifier: Modifier = Modifier, ) { - Surface( - modifier = modifier.fillMaxWidth(), - onClick = onClick, - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 12.dp), - verticalAlignment = Alignment.CenterVertically, + Column(modifier = modifier) { + Surface( + modifier = Modifier.fillMaxWidth(), + onClick = onClick, ) { - Icon( - imageVector = icon, - contentDescription = null, - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - Spacer(modifier = Modifier.width(16.dp)) - Column(modifier = Modifier.weight(1f)) { - Text( - text = title, - style = MaterialTheme.typography.bodyLarge, + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, ) - Text( - text = subtitle, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.bodyLarge, + ) + Text( + text = subtitle, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, ) } - Icon( - imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - contentDescription = null, - modifier = Modifier.size(20.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) } + HorizontalDivider() } - HorizontalDivider() } diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/ChatSettingsSection.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/ChatSettingsSection.kt index 339386b..1412295 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/ChatSettingsSection.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/ChatSettingsSection.kt @@ -18,13 +18,13 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.semantics.Role import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp import com.librechat.android.core.common.ChatLayoutConstants -import com.librechat.android.feature.settings.R import com.librechat.android.core.data.datastore.ChatFontSize import com.librechat.android.core.data.datastore.LatexRenderer +import com.librechat.android.feature.settings.R @Composable internal fun ChatSettingsSection( @@ -48,283 +48,288 @@ internal fun ChatSettingsSection( onLatexRendererChange: (LatexRenderer) -> Unit, modifier: Modifier = Modifier, ) { - Column( - modifier = modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - ) { - // Chat layout style selector - Text( - text = stringResource(R.string.chat_layout), - style = MaterialTheme.typography.bodyLarge, - ) - Spacer(modifier = Modifier.height(4.dp)) - listOf(ChatLayoutConstants.THREAD to stringResource(R.string.chat_layout_thread), ChatLayoutConstants.TWO_SIDED to stringResource(R.string.chat_layout_two_sided)).forEach { (value, label) -> - Row( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(8.dp)) - .selectable( + Column(modifier = modifier) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + ) { + // Chat layout style selector + Text( + text = stringResource(R.string.chat_layout), + style = MaterialTheme.typography.bodyLarge, + ) + Spacer(modifier = Modifier.height(4.dp)) + listOf( + ChatLayoutConstants.THREAD to stringResource(R.string.chat_layout_thread), + ChatLayoutConstants.TWO_SIDED to stringResource(R.string.chat_layout_two_sided), + ).forEach { (value, label) -> + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .selectable( + selected = chatLayoutStyle == value, + onClick = { onChatLayoutStyleChange(value) }, + role = Role.RadioButton, + ) + .padding(vertical = 8.dp, horizontal = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + RadioButton( selected = chatLayoutStyle == value, - onClick = { onChatLayoutStyleChange(value) }, - role = Role.RadioButton, + onClick = null, ) - .padding(vertical = 8.dp, horizontal = 4.dp), + Spacer(modifier = Modifier.width(8.dp)) + Column { + Text( + text = label, + style = MaterialTheme.typography.bodyLarge, + ) + Text( + text = if (value == ChatLayoutConstants.THREAD) { + stringResource(R.string.chat_layout_thread_desc) + } else { + stringResource(R.string.chat_layout_two_sided_desc) + }, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + + Spacer(modifier = Modifier.height(12.dp)) + + // Show bubbles toggle + Row( + modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { - RadioButton( - selected = chatLayoutStyle == value, - onClick = null, - ) - Spacer(modifier = Modifier.width(8.dp)) - Column { + Column(modifier = Modifier.weight(1f)) { Text( - text = label, + text = stringResource(R.string.show_bubbles), style = MaterialTheme.typography.bodyLarge, ) Text( - text = if (value == ChatLayoutConstants.THREAD) { - stringResource(R.string.chat_layout_thread_desc) - } else { - stringResource(R.string.chat_layout_two_sided_desc) - }, + text = stringResource(R.string.show_bubbles_desc), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) } - } - } - - Spacer(modifier = Modifier.height(12.dp)) - - // Show bubbles toggle - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - ) { - Column(modifier = Modifier.weight(1f)) { - Text( - text = stringResource(R.string.show_bubbles), - style = MaterialTheme.typography.bodyLarge, - ) - Text( - text = stringResource(R.string.show_bubbles_desc), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, + Spacer(modifier = Modifier.width(16.dp)) + Switch( + checked = showBubbles, + onCheckedChange = onShowBubblesChange, ) } - Spacer(modifier = Modifier.width(16.dp)) - Switch( - checked = showBubbles, - onCheckedChange = onShowBubblesChange, - ) - } - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(12.dp)) - // Show avatars toggle - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - ) { - Column(modifier = Modifier.weight(1f)) { - Text( - text = stringResource(R.string.show_avatars), - style = MaterialTheme.typography.bodyLarge, - ) - Text( - text = stringResource(R.string.show_avatars_desc), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - Spacer(modifier = Modifier.width(16.dp)) - Switch( - checked = showAvatars, - onCheckedChange = onShowAvatarsChange, - ) - } - - Spacer(modifier = Modifier.height(12.dp)) - - // Font size selector - Text( - text = stringResource(R.string.font_size), - style = MaterialTheme.typography.bodyLarge, - ) - Spacer(modifier = Modifier.height(4.dp)) - ChatFontSize.entries.forEach { size -> + // Show avatars toggle Row( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(8.dp)) - .selectable( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(R.string.show_avatars), + style = MaterialTheme.typography.bodyLarge, + ) + Text( + text = stringResource(R.string.show_avatars_desc), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Spacer(modifier = Modifier.width(16.dp)) + Switch( + checked = showAvatars, + onCheckedChange = onShowAvatarsChange, + ) + } + + Spacer(modifier = Modifier.height(12.dp)) + + // Font size selector + Text( + text = stringResource(R.string.font_size), + style = MaterialTheme.typography.bodyLarge, + ) + Spacer(modifier = Modifier.height(4.dp)) + ChatFontSize.entries.forEach { size -> + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .selectable( + selected = fontSize == size, + onClick = { onFontSizeChange(size) }, + role = Role.RadioButton, + ) + .padding(vertical = 8.dp, horizontal = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + RadioButton( selected = fontSize == size, - onClick = { onFontSizeChange(size) }, - role = Role.RadioButton, + onClick = null, ) - .padding(vertical = 8.dp, horizontal = 4.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - RadioButton( - selected = fontSize == size, - onClick = null, - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = when (size) { - ChatFontSize.SMALL -> stringResource(R.string.font_size_small) - ChatFontSize.MEDIUM -> stringResource(R.string.font_size_medium) - ChatFontSize.LARGE -> stringResource(R.string.font_size_large) - }, - style = MaterialTheme.typography.bodyLarge, - ) - } - } - - Spacer(modifier = Modifier.height(12.dp)) - - // LaTeX renderer selector - Text( - text = stringResource(R.string.latex_renderer), - style = MaterialTheme.typography.bodyLarge, - ) - Spacer(modifier = Modifier.height(4.dp)) - listOf( - LatexRenderer.KATEX to (stringResource(R.string.latex_katex) to stringResource(R.string.latex_katex_desc)), - LatexRenderer.NATIVE to (stringResource(R.string.latex_native) to stringResource(R.string.latex_native_desc)), - ).forEach { (renderer, labelAndDesc) -> - val (label, description) = labelAndDesc - Row( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(8.dp)) - .selectable( - selected = latexRenderer == renderer, - onClick = { onLatexRendererChange(renderer) }, - role = Role.RadioButton, - ) - .padding(vertical = 8.dp, horizontal = 4.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - RadioButton( - selected = latexRenderer == renderer, - onClick = null, - ) - Spacer(modifier = Modifier.width(8.dp)) - Column { + Spacer(modifier = Modifier.width(8.dp)) Text( - text = label, + text = when (size) { + ChatFontSize.SMALL -> stringResource(R.string.font_size_small) + ChatFontSize.MEDIUM -> stringResource(R.string.font_size_medium) + ChatFontSize.LARGE -> stringResource(R.string.font_size_large) + }, + style = MaterialTheme.typography.bodyLarge, + ) + } + } + + Spacer(modifier = Modifier.height(12.dp)) + + // LaTeX renderer selector + Text( + text = stringResource(R.string.latex_renderer), + style = MaterialTheme.typography.bodyLarge, + ) + Spacer(modifier = Modifier.height(4.dp)) + listOf( + LatexRenderer.KATEX to (stringResource(R.string.latex_katex) to stringResource(R.string.latex_katex_desc)), + LatexRenderer.NATIVE to (stringResource(R.string.latex_native) to stringResource(R.string.latex_native_desc)), + ).forEach { (renderer, labelAndDesc) -> + val (label, description) = labelAndDesc + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .selectable( + selected = latexRenderer == renderer, + onClick = { onLatexRendererChange(renderer) }, + role = Role.RadioButton, + ) + .padding(vertical = 8.dp, horizontal = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + RadioButton( + selected = latexRenderer == renderer, + onClick = null, + ) + Spacer(modifier = Modifier.width(8.dp)) + Column { + Text( + text = label, + style = MaterialTheme.typography.bodyLarge, + ) + Text( + text = description, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + + Spacer(modifier = Modifier.height(12.dp)) + + // Auto-scroll toggle + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(R.string.auto_scroll), style = MaterialTheme.typography.bodyLarge, ) Text( - text = description, + text = stringResource(R.string.auto_scroll_desc), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) } - } - } - - Spacer(modifier = Modifier.height(12.dp)) - - // Auto-scroll toggle - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - ) { - Column(modifier = Modifier.weight(1f)) { - Text( - text = stringResource(R.string.auto_scroll), - style = MaterialTheme.typography.bodyLarge, - ) - Text( - text = stringResource(R.string.auto_scroll_desc), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, + Spacer(modifier = Modifier.width(16.dp)) + Switch( + checked = autoScrollEnabled, + onCheckedChange = onAutoScrollChange, ) } - Spacer(modifier = Modifier.width(16.dp)) - Switch( - checked = autoScrollEnabled, - onCheckedChange = onAutoScrollChange, - ) - } - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(12.dp)) - // Show thinking blocks toggle - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - ) { - Column(modifier = Modifier.weight(1f)) { - Text( - text = stringResource(R.string.show_thinking_blocks), - style = MaterialTheme.typography.bodyLarge, - ) - Text( - text = stringResource(R.string.show_thinking_blocks_desc), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, + // Show thinking blocks toggle + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(R.string.show_thinking_blocks), + style = MaterialTheme.typography.bodyLarge, + ) + Text( + text = stringResource(R.string.show_thinking_blocks_desc), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Spacer(modifier = Modifier.width(16.dp)) + Switch( + checked = showThinkingBlocks, + onCheckedChange = onShowThinkingChange, ) } - Spacer(modifier = Modifier.width(16.dp)) - Switch( - checked = showThinkingBlocks, - onCheckedChange = onShowThinkingChange, - ) - } - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(12.dp)) - // Show image descriptions toggle - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - ) { - Column(modifier = Modifier.weight(1f)) { - Text( - text = stringResource(R.string.show_image_descriptions), - style = MaterialTheme.typography.bodyLarge, - ) - Text( - text = stringResource(R.string.show_image_descriptions_desc), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, + // Show image descriptions toggle + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(R.string.show_image_descriptions), + style = MaterialTheme.typography.bodyLarge, + ) + Text( + text = stringResource(R.string.show_image_descriptions_desc), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Spacer(modifier = Modifier.width(16.dp)) + Switch( + checked = showImageDescriptions, + onCheckedChange = onShowImageDescriptionsChange, ) } - Spacer(modifier = Modifier.width(16.dp)) - Switch( - checked = showImageDescriptions, - onCheckedChange = onShowImageDescriptionsChange, - ) - } - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(12.dp)) - // Dismiss keyboard on send toggle - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - ) { - Column(modifier = Modifier.weight(1f)) { - Text( - text = stringResource(R.string.dismiss_keyboard_on_send), - style = MaterialTheme.typography.bodyLarge, - ) - Text( - text = stringResource(R.string.dismiss_keyboard_on_send_desc), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, + // Dismiss keyboard on send toggle + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(R.string.dismiss_keyboard_on_send), + style = MaterialTheme.typography.bodyLarge, + ) + Text( + text = stringResource(R.string.dismiss_keyboard_on_send_desc), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Spacer(modifier = Modifier.width(16.dp)) + Switch( + checked = dismissKeyboardOnSend, + onCheckedChange = onDismissKeyboardOnSendChange, ) } - Spacer(modifier = Modifier.width(16.dp)) - Switch( - checked = dismissKeyboardOnSend, - onCheckedChange = onDismissKeyboardOnSendChange, - ) } + HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) } - HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) } diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/CommandsConfigScreen.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/CommandsConfigScreen.kt index c2c94d5..e136442 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/CommandsConfigScreen.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/CommandsConfigScreen.kt @@ -24,8 +24,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import com.librechat.android.feature.settings.R import androidx.compose.ui.unit.dp +import com.librechat.android.feature.settings.R data class CommandConfig( val name: String, diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/DataSettingsScreen.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/DataSettingsScreen.kt index e397f03..b6b818c 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/DataSettingsScreen.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/DataSettingsScreen.kt @@ -9,7 +9,6 @@ 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.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack @@ -36,14 +35,14 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.librechat.android.feature.settings.R import com.librechat.android.feature.settings.viewmodel.SettingsViewModel +import org.koin.compose.viewmodel.koinViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -121,133 +120,135 @@ fun DataSettingsContent( viewModel.dismissMcpReinitializeMessage() } - LazyColumn( - modifier = modifier.fillMaxSize(), - ) { - // Conversations section - item(key = "conversations_header") { - SectionHeader(stringResource(R.string.section_conversations)) + Column { + LazyColumn( + modifier = modifier.fillMaxSize(), + ) { + // Conversations section + item(key = "conversations_header") { + SectionHeader(stringResource(R.string.section_conversations)) + } + item(key = "data_settings") { + DataSettingsSection( + archivedCount = uiState.archivedCount, + isClearing = uiState.isClearing, + onClearAllChats = viewModel::clearAllChats, + onViewArchived = onNavigateToArchived, + onExportAllData = viewModel::exportAllData, + ) + } + item(key = "data_extra_actions") { + DataExtraActions( + onSharedLinksClick = onNavigateToSharedLinks, + onClearCacheClick = { showClearCacheDialog = true }, + isCacheClearing = uiState.isCacheClearing, + onRevokeKeysClick = { showRevokeKeysDialog = true }, + isKeyRevoking = uiState.isKeyRevoking, + ) + } + + // Memories section + item(key = "memories_header") { + SectionHeader(stringResource(R.string.section_memories)) + } + item(key = "memories_settings") { + MemoriesSettingsSection( + memories = uiState.memories, + memoriesEnabled = uiState.memoriesEnabled, + showMemoryDialog = uiState.showMemoryDialog, + editingMemory = uiState.editingMemory, + onToggleEnabled = viewModel::toggleMemoriesEnabled, + onAddMemory = viewModel::showAddMemoryDialog, + onEditMemory = viewModel::showEditMemoryDialog, + onDeleteMemory = viewModel::deleteMemory, + onDismissDialog = viewModel::dismissMemoryDialog, + onSaveMemory = viewModel::saveMemory, + ) + } + + // MCP section + item(key = "mcp_header") { + SectionHeader(stringResource(R.string.section_mcp_servers)) + } + item(key = "mcp_settings") { + McpSettingsSection( + servers = uiState.mcpServers, + connectionStatus = uiState.mcpConnectionStatus, + reinitializingServers = uiState.mcpReinitializingServers, + error = uiState.mcpError, + onAddServer = viewModel::showAddMcpServerDialog, + onEditServer = viewModel::showEditMcpServerDialog, + onDeleteServer = viewModel::deleteMcpServer, + onReinitialize = viewModel::reinitializeMcpServer, + ) + } + + // Bottom spacing + item { Spacer(modifier = Modifier.height(32.dp)) } } - item(key = "data_settings") { - DataSettingsSection( - archivedCount = uiState.archivedCount, - isClearing = uiState.isClearing, - onClearAllChats = viewModel::clearAllChats, - onViewArchived = onNavigateToArchived, - onExportAllData = viewModel::exportAllData, - ) - } - item(key = "data_extra_actions") { - DataExtraActions( - onSharedLinksClick = onNavigateToSharedLinks, - onClearCacheClick = { showClearCacheDialog = true }, - isCacheClearing = uiState.isCacheClearing, - onRevokeKeysClick = { showRevokeKeysDialog = true }, - isKeyRevoking = uiState.isKeyRevoking, + + // MCP server add/edit dialog + if (uiState.showMcpServerDialog) { + McpServerDialog( + editingServer = uiState.editingMcpServer, + onDismiss = viewModel::dismissMcpServerDialog, + onSave = { name, description, url, type, apiKey, oauth -> + viewModel.saveMcpServer(name, description, url, type, apiKey, oauth) + }, ) } - // Memories section - item(key = "memories_header") { - SectionHeader(stringResource(R.string.section_memories)) - } - item(key = "memories_settings") { - MemoriesSettingsSection( - memories = uiState.memories, - memoriesEnabled = uiState.memoriesEnabled, - showMemoryDialog = uiState.showMemoryDialog, - editingMemory = uiState.editingMemory, - onToggleEnabled = viewModel::toggleMemoriesEnabled, - onAddMemory = viewModel::showAddMemoryDialog, - onEditMemory = viewModel::showEditMemoryDialog, - onDeleteMemory = viewModel::deleteMemory, - onDismissDialog = viewModel::dismissMemoryDialog, - onSaveMemory = viewModel::saveMemory, + // Clear cache confirmation + if (showClearCacheDialog) { + AlertDialog( + onDismissRequest = { showClearCacheDialog = false }, + title = { Text(stringResource(R.string.dialog_title_clear_cache)) }, + text = { Text(stringResource(R.string.dialog_clear_cache_message)) }, + confirmButton = { + TextButton( + onClick = { + showClearCacheDialog = false + viewModel.clearCache() + }, + ) { + Text(stringResource(R.string.action_clear)) + } + }, + dismissButton = { + TextButton(onClick = { showClearCacheDialog = false }) { + Text(stringResource(R.string.action_cancel)) + } + }, ) } - // MCP section - item(key = "mcp_header") { - SectionHeader(stringResource(R.string.section_mcp_servers)) - } - item(key = "mcp_settings") { - McpSettingsSection( - servers = uiState.mcpServers, - connectionStatus = uiState.mcpConnectionStatus, - reinitializingServers = uiState.mcpReinitializingServers, - error = uiState.mcpError, - onAddServer = viewModel::showAddMcpServerDialog, - onEditServer = viewModel::showEditMcpServerDialog, - onDeleteServer = viewModel::deleteMcpServer, - onReinitialize = viewModel::reinitializeMcpServer, + // Revoke keys confirmation + if (showRevokeKeysDialog) { + AlertDialog( + onDismissRequest = { showRevokeKeysDialog = false }, + title = { Text(stringResource(R.string.dialog_title_revoke_keys)) }, + text = { Text(stringResource(R.string.dialog_revoke_keys_message)) }, + confirmButton = { + TextButton( + onClick = { + showRevokeKeysDialog = false + viewModel.revokeAllKeys() + }, + colors = ButtonDefaults.textButtonColors( + contentColor = MaterialTheme.colorScheme.error, + ), + ) { + Text(stringResource(R.string.action_revoke_all)) + } + }, + dismissButton = { + TextButton(onClick = { showRevokeKeysDialog = false }) { + Text(stringResource(R.string.action_cancel)) + } + }, ) } - - // Bottom spacing - item { Spacer(modifier = Modifier.height(32.dp)) } - } - - // MCP server add/edit dialog - if (uiState.showMcpServerDialog) { - McpServerDialog( - editingServer = uiState.editingMcpServer, - onDismiss = viewModel::dismissMcpServerDialog, - onSave = { name, description, url, type, apiKey, oauth -> - viewModel.saveMcpServer(name, description, url, type, apiKey, oauth) - }, - ) - } - - // Clear cache confirmation - if (showClearCacheDialog) { - AlertDialog( - onDismissRequest = { showClearCacheDialog = false }, - title = { Text(stringResource(R.string.dialog_title_clear_cache)) }, - text = { Text(stringResource(R.string.dialog_clear_cache_message)) }, - confirmButton = { - TextButton( - onClick = { - showClearCacheDialog = false - viewModel.clearCache() - }, - ) { - Text(stringResource(R.string.action_clear)) - } - }, - dismissButton = { - TextButton(onClick = { showClearCacheDialog = false }) { - Text(stringResource(R.string.action_cancel)) - } - }, - ) - } - - // Revoke keys confirmation - if (showRevokeKeysDialog) { - AlertDialog( - onDismissRequest = { showRevokeKeysDialog = false }, - title = { Text(stringResource(R.string.dialog_title_revoke_keys)) }, - text = { Text(stringResource(R.string.dialog_revoke_keys_message)) }, - confirmButton = { - TextButton( - onClick = { - showRevokeKeysDialog = false - viewModel.revokeAllKeys() - }, - colors = ButtonDefaults.textButtonColors( - contentColor = MaterialTheme.colorScheme.error, - ), - ) { - Text(stringResource(R.string.action_revoke_all)) - } - }, - dismissButton = { - TextButton(onClick = { showRevokeKeysDialog = false }) { - Text(stringResource(R.string.action_cancel)) - } - }, - ) - } + } // Column } @Composable @@ -270,51 +271,53 @@ private fun DataExtraActions( onRevokeKeysClick: () -> Unit, isKeyRevoking: Boolean, ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 4.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - // Shared Links - OutlinedButton( - onClick = onSharedLinksClick, - modifier = Modifier.fillMaxWidth(), + Column { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 4.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), ) { - Row( + // Shared Links + OutlinedButton( + onClick = onSharedLinksClick, modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, ) { - Text(stringResource(R.string.shared_links)) - Icon( - imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text(stringResource(R.string.shared_links)) + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + } + } + + // Clear cache + OutlinedButton( + onClick = onClearCacheClick, + enabled = !isCacheClearing, + modifier = Modifier.fillMaxWidth(), + ) { + Text(stringResource(if (isCacheClearing) R.string.clearing else R.string.clear_cache)) + } + + // Revoke API keys + OutlinedButton( + onClick = onRevokeKeysClick, + enabled = !isKeyRevoking, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MaterialTheme.colorScheme.error, + ), + ) { + Text(stringResource(if (isKeyRevoking) R.string.revoking else R.string.revoke_all_api_keys)) } } - - // Clear cache - OutlinedButton( - onClick = onClearCacheClick, - enabled = !isCacheClearing, - modifier = Modifier.fillMaxWidth(), - ) { - Text(stringResource(if (isCacheClearing) R.string.clearing else R.string.clear_cache)) - } - - // Revoke API keys - OutlinedButton( - onClick = onRevokeKeysClick, - enabled = !isKeyRevoking, - modifier = Modifier.fillMaxWidth(), - colors = ButtonDefaults.outlinedButtonColors( - contentColor = MaterialTheme.colorScheme.error, - ), - ) { - Text(stringResource(if (isKeyRevoking) R.string.revoking else R.string.revoke_all_api_keys)) - } + HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) } - HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) } diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/DataSettingsSection.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/DataSettingsSection.kt index 6de6c9a..3ba8fc3 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/DataSettingsSection.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/DataSettingsSection.kt @@ -28,8 +28,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import com.librechat.android.feature.settings.R import androidx.compose.ui.unit.dp +import com.librechat.android.feature.settings.R @Composable internal fun DataSettingsSection( @@ -42,106 +42,108 @@ internal fun DataSettingsSection( ) { var showClearDialog by remember { mutableStateOf(false) } - Column( - modifier = modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), - ) { - // Clear all conversations - Button( - onClick = { showClearDialog = true }, - enabled = !isClearing, - modifier = Modifier.fillMaxWidth(), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.error, - contentColor = MaterialTheme.colorScheme.onError, - ), + Column(modifier = modifier) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), ) { - Text(stringResource(if (isClearing) R.string.clearing else R.string.clear_all_conversations)) - } - - // Archived conversations - OutlinedButton( - onClick = onViewArchived, - modifier = Modifier.fillMaxWidth(), - ) { - Row( + // Clear all conversations + Button( + onClick = { showClearDialog = true }, + enabled = !isClearing, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.error, + contentColor = MaterialTheme.colorScheme.onError, + ), + ) { + Text(stringResource(if (isClearing) R.string.clearing else R.string.clear_all_conversations)) + } + + // Archived conversations + OutlinedButton( + onClick = onViewArchived, modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, ) { Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - Icon( - imageVector = Icons.Default.Archive, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - Text(stringResource(R.string.archived_conversations)) - } - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), - ) { - if (archivedCount > 0) { - Text( - text = "$archivedCount", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Icon( + imageVector = Icons.Default.Archive, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + Text(stringResource(R.string.archived_conversations)) + } + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + if (archivedCount > 0) { + Text( + text = "$archivedCount", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null, + modifier = Modifier.size(18.dp), ) } - Icon( - imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) } } + + // Export all data + OutlinedButton( + onClick = onExportAllData, + modifier = Modifier.fillMaxWidth(), + ) { + Text(stringResource(R.string.export_all_data)) + } + + Spacer(modifier = Modifier.height(0.dp)) } + HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) - // Export all data - OutlinedButton( - onClick = onExportAllData, - modifier = Modifier.fillMaxWidth(), - ) { - Text(stringResource(R.string.export_all_data)) + // Clear all chats confirmation dialog + if (showClearDialog) { + AlertDialog( + onDismissRequest = { showClearDialog = false }, + title = { Text(stringResource(R.string.dialog_title_clear_conversations)) }, + text = { + Text( + stringResource(R.string.dialog_clear_conversations_message), + ) + }, + confirmButton = { + TextButton( + onClick = { + showClearDialog = false + onClearAllChats() + }, + colors = ButtonDefaults.textButtonColors( + contentColor = MaterialTheme.colorScheme.error, + ), + ) { + Text(stringResource(R.string.clear_all)) + } + }, + dismissButton = { + TextButton(onClick = { showClearDialog = false }) { + Text(stringResource(R.string.action_cancel)) + } + }, + ) } - - Spacer(modifier = Modifier.height(0.dp)) - } - HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) - - // Clear all chats confirmation dialog - if (showClearDialog) { - AlertDialog( - onDismissRequest = { showClearDialog = false }, - title = { Text(stringResource(R.string.dialog_title_clear_conversations)) }, - text = { - Text( - stringResource(R.string.dialog_clear_conversations_message), - ) - }, - confirmButton = { - TextButton( - onClick = { - showClearDialog = false - onClearAllChats() - }, - colors = ButtonDefaults.textButtonColors( - contentColor = MaterialTheme.colorScheme.error, - ), - ) { - Text(stringResource(R.string.clear_all)) - } - }, - dismissButton = { - TextButton(onClick = { showClearDialog = false }) { - Text(stringResource(R.string.action_cancel)) - } - }, - ) - } + } // Column } diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/ForkSettingsDialog.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/ForkSettingsDialog.kt index 2792e74..26d9337 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/ForkSettingsDialog.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/ForkSettingsDialog.kt @@ -21,11 +21,11 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.semantics.Role import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp -import com.librechat.android.feature.settings.R import com.librechat.android.core.model.request.ForkOption +import com.librechat.android.feature.settings.R enum class ForkMode(val label: String, val description: String, val apiValue: String) { DIRECT_PATH( diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/GeneralSettingsScreen.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/GeneralSettingsScreen.kt index 958e65e..ce85d77 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/GeneralSettingsScreen.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/GeneralSettingsScreen.kt @@ -33,17 +33,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.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.librechat.android.feature.settings.R import com.librechat.android.core.data.datastore.ThemeMode +import com.librechat.android.feature.settings.R import com.librechat.android.feature.settings.viewmodel.SettingsViewModel +import org.koin.compose.viewmodel.koinViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -88,88 +88,94 @@ fun GeneralSettingsContent( ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() - LazyColumn( - modifier = modifier.fillMaxSize(), - ) { - // Appearance section - item(key = "appearance_header") { - SectionHeader(stringResource(R.string.section_appearance)) + Column { + LazyColumn( + modifier = modifier.fillMaxSize(), + ) { + // Appearance section + item(key = "appearance_header") { + SectionHeader(stringResource(R.string.section_appearance)) + } + item(key = "theme_selector") { + ThemeSelector( + selected = uiState.themeMode, + onSelect = viewModel::setThemeMode, + ) + } + + // Language + item(key = "general_header") { + SectionHeader(stringResource(R.string.section_language)) + } + item(key = "language_row") { + GeneralSettingsRow( + icon = Icons.Default.Language, + title = stringResource(R.string.language), + subtitle = uiState.selectedLanguage.uppercase(), + onClick = viewModel::showLanguageDialog, + ) + } + + // Tablet section + item(key = "tablet_header") { + SectionHeader(stringResource(R.string.section_layout)) + } + item(key = "tablet_sidebar_gesture") { + TabletSidebarGestureToggle( + gestureEnabled = uiState.tabletSidebarGestureEnabled, + onGestureEnabledChange = viewModel::setTabletSidebarGestureEnabled, + ) + } + + // Personalization + item(key = "personalization_header") { + SectionHeader(stringResource(R.string.section_personalization)) + } + item(key = "personalization_row") { + GeneralSettingsRow( + icon = Icons.Default.Person, + title = stringResource(R.string.personalization), + subtitle = if (uiState.personalizationEnabled) { + stringResource(R.string.status_enabled) + } else { + stringResource(R.string.status_disabled) + }, + onClick = viewModel::showPersonalizationDialog, + ) + } + + // About section + item(key = "about_header") { + SectionHeader(stringResource(R.string.section_about)) + } + item(key = "about_info") { + AboutInfo(serverUrl = uiState.serverUrl) + } + + // Bottom spacing + item { Spacer(modifier = Modifier.height(32.dp)) } } - item(key = "theme_selector") { - ThemeSelector( - selected = uiState.themeMode, - onSelect = viewModel::setThemeMode, + + // Language selector dialog + if (uiState.showLanguageDialog) { + LanguageSelectorDialog( + selectedLanguage = uiState.selectedLanguage, + onLanguageSelected = viewModel::setLanguage, + onDismiss = viewModel::dismissLanguageDialog, ) } - // Language - item(key = "general_header") { - SectionHeader(stringResource(R.string.section_language)) - } - item(key = "language_row") { - GeneralSettingsRow( - icon = Icons.Default.Language, - title = stringResource(R.string.language), - subtitle = uiState.selectedLanguage.uppercase(), - onClick = viewModel::showLanguageDialog, + // Personalization dialog + if (uiState.showPersonalizationDialog) { + PersonalizationDialog( + aboutUser = uiState.aboutUser, + responseStyle = uiState.responseStyle, + enabled = uiState.personalizationEnabled, + onSave = viewModel::savePersonalization, + onDismiss = viewModel::dismissPersonalizationDialog, ) } - - // Tablet section - item(key = "tablet_header") { - SectionHeader(stringResource(R.string.section_layout)) - } - item(key = "tablet_sidebar_gesture") { - TabletSidebarGestureToggle( - gestureEnabled = uiState.tabletSidebarGestureEnabled, - onGestureEnabledChange = viewModel::setTabletSidebarGestureEnabled, - ) - } - - // Personalization - item(key = "personalization_header") { - SectionHeader(stringResource(R.string.section_personalization)) - } - item(key = "personalization_row") { - GeneralSettingsRow( - icon = Icons.Default.Person, - title = stringResource(R.string.personalization), - subtitle = if (uiState.personalizationEnabled) stringResource(R.string.status_enabled) else stringResource(R.string.status_disabled), - onClick = viewModel::showPersonalizationDialog, - ) - } - - // About section - item(key = "about_header") { - SectionHeader(stringResource(R.string.section_about)) - } - item(key = "about_info") { - AboutInfo(serverUrl = uiState.serverUrl) - } - - // Bottom spacing - item { Spacer(modifier = Modifier.height(32.dp)) } - } - - // Language selector dialog - if (uiState.showLanguageDialog) { - LanguageSelectorDialog( - selectedLanguage = uiState.selectedLanguage, - onLanguageSelected = viewModel::setLanguage, - onDismiss = viewModel::dismissLanguageDialog, - ) - } - - // Personalization dialog - if (uiState.showPersonalizationDialog) { - PersonalizationDialog( - aboutUser = uiState.aboutUser, - responseStyle = uiState.responseStyle, - enabled = uiState.personalizationEnabled, - onSave = viewModel::savePersonalization, - onDismiss = viewModel::dismissPersonalizationDialog, - ) - } + } // Column } @Composable @@ -189,37 +195,39 @@ private fun ThemeSelector( selected: ThemeMode, onSelect: (ThemeMode) -> Unit, ) { - Column(modifier = Modifier.padding(horizontal = 16.dp)) { - ThemeMode.entries.forEach { mode -> - Row( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(8.dp)) - .selectable( + Column { + Column(modifier = Modifier.padding(horizontal = 16.dp)) { + ThemeMode.entries.forEach { mode -> + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .selectable( + selected = selected == mode, + onClick = { onSelect(mode) }, + role = Role.RadioButton, + ) + .padding(vertical = 8.dp, horizontal = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + RadioButton( selected = selected == mode, - onClick = { onSelect(mode) }, - role = Role.RadioButton, + onClick = null, ) - .padding(vertical = 8.dp, horizontal = 4.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - RadioButton( - selected = selected == mode, - onClick = null, - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = when (mode) { - ThemeMode.SYSTEM -> stringResource(R.string.theme_system) - ThemeMode.LIGHT -> stringResource(R.string.theme_light) - ThemeMode.DARK -> stringResource(R.string.theme_dark) - }, - style = MaterialTheme.typography.bodyLarge, - ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = when (mode) { + ThemeMode.SYSTEM -> stringResource(R.string.theme_system) + ThemeMode.LIGHT -> stringResource(R.string.theme_light) + ThemeMode.DARK -> stringResource(R.string.theme_dark) + }, + style = MaterialTheme.typography.bodyLarge, + ) + } } } + HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) } - HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) } @Composable @@ -228,84 +236,88 @@ private fun TabletSidebarGestureToggle( onGestureEnabledChange: (Boolean) -> Unit, modifier: Modifier = Modifier, ) { - Column( - modifier = modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, + Column(modifier = modifier) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), ) { - Icon( - imageVector = Icons.Default.Tablet, - contentDescription = null, - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - Spacer(modifier = Modifier.width(16.dp)) - Column(modifier = Modifier.weight(1f)) { - Text( - text = stringResource(R.string.sidebar_swipe_gesture), - style = MaterialTheme.typography.bodyLarge, + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Default.Tablet, + contentDescription = null, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, ) - Text( - text = stringResource(R.string.sidebar_swipe_gesture_desc), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(R.string.sidebar_swipe_gesture), + style = MaterialTheme.typography.bodyLarge, + ) + Text( + text = stringResource(R.string.sidebar_swipe_gesture_desc), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Spacer(modifier = Modifier.width(16.dp)) + Switch( + checked = gestureEnabled, + onCheckedChange = onGestureEnabledChange, ) } - Spacer(modifier = Modifier.width(16.dp)) - Switch( - checked = gestureEnabled, - onCheckedChange = onGestureEnabledChange, - ) } + HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) } - HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) } @Composable private fun AboutInfo(serverUrl: String) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = androidx.compose.foundation.layout.Arrangement.SpaceBetween, + Column { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), ) { - Text( - text = stringResource(R.string.app_version_label), - style = MaterialTheme.typography.bodyMedium, - ) - Text( - text = stringResource(R.string.app_version_value), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - Spacer(modifier = Modifier.height(8.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = androidx.compose.foundation.layout.Arrangement.SpaceBetween, - ) { - Text( - text = stringResource(R.string.server_label), - style = MaterialTheme.typography.bodyMedium, - ) - Text( - text = serverUrl.ifBlank { stringResource(R.string.server_not_configured) }, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.weight(1f, fill = false), - ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = androidx.compose.foundation.layout.Arrangement.SpaceBetween, + ) { + Text( + text = stringResource(R.string.app_version_label), + style = MaterialTheme.typography.bodyMedium, + ) + Text( + text = stringResource(R.string.app_version_value), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = androidx.compose.foundation.layout.Arrangement.SpaceBetween, + ) { + Text( + text = stringResource(R.string.server_label), + style = MaterialTheme.typography.bodyMedium, + ) + Text( + text = serverUrl.ifBlank { stringResource(R.string.server_not_configured) }, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f, fill = false), + ) + } } + HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) } - HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) } @Composable @@ -316,41 +328,43 @@ private fun GeneralSettingsRow( onClick: () -> Unit, modifier: Modifier = Modifier, ) { - androidx.compose.material3.Surface( - modifier = modifier.fillMaxWidth(), - onClick = onClick, - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 12.dp), - verticalAlignment = Alignment.CenterVertically, + Column(modifier = modifier) { + androidx.compose.material3.Surface( + modifier = Modifier.fillMaxWidth(), + onClick = onClick, ) { - Icon( - imageVector = icon, - contentDescription = null, - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - Spacer(modifier = Modifier.width(16.dp)) - Column(modifier = Modifier.weight(1f)) { - Text( - text = title, - style = MaterialTheme.typography.bodyLarge, + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, ) - Text( - text = subtitle, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.bodyLarge, + ) + Text( + text = subtitle, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, ) } - Icon( - imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - contentDescription = null, - modifier = Modifier.size(20.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) } + HorizontalDivider() } - HorizontalDivider() } diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/LanguageSelectorDialog.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/LanguageSelectorDialog.kt index ceb12af..040b326 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/LanguageSelectorDialog.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/LanguageSelectorDialog.kt @@ -26,8 +26,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import com.librechat.android.feature.settings.R import androidx.compose.ui.unit.dp +import com.librechat.android.feature.settings.R data class LanguageOption( val code: String, diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/McpServerDialog.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/McpServerDialog.kt index 123ad72..d49a0fd 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/McpServerDialog.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/McpServerDialog.kt @@ -26,10 +26,9 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.dp -import com.librechat.android.feature.settings.R import com.librechat.android.core.model.mcp.McpApiKeyConfig import com.librechat.android.core.model.mcp.McpApiKeySource import com.librechat.android.core.model.mcp.McpAuthMode @@ -37,6 +36,7 @@ import com.librechat.android.core.model.mcp.McpAuthorizationType import com.librechat.android.core.model.mcp.McpOAuthConfig import com.librechat.android.core.model.mcp.McpServer import com.librechat.android.core.model.mcp.McpServerType +import com.librechat.android.feature.settings.R /** Add/edit MCP server dialog with server type dropdown and auth configuration. */ @OptIn(ExperimentalMaterial3Api::class) @@ -44,7 +44,8 @@ import com.librechat.android.core.model.mcp.McpServerType internal fun McpServerDialog( editingServer: McpServer?, onDismiss: () -> Unit, - onSave: (name: String, description: String?, url: String, type: McpServerType, apiKey: McpApiKeyConfig?, oauth: McpOAuthConfig?) -> Unit, + onSave: + (name: String, description: String?, url: String, type: McpServerType, apiKey: McpApiKeyConfig?, oauth: McpOAuthConfig?) -> Unit, modifier: Modifier = Modifier, ) { var name by remember { mutableStateOf(editingServer?.title ?: editingServer?.name ?: "") } diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/McpServerStatusIndicator.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/McpServerStatusIndicator.kt index 44718c5..7c3d974 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/McpServerStatusIndicator.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/McpServerStatusIndicator.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -17,9 +16,9 @@ internal fun McpServerStatusIndicator( modifier: Modifier = Modifier, ) { val color = when (isConnected) { - true -> Color(0xFF4CAF50) // Green - false -> Color(0xFFF44336) // Red - null -> Color(0xFF9E9E9E) // Gray + true -> Color(0xFF4CAF50) // Green + false -> Color(0xFFF44336) // Red + null -> Color(0xFF9E9E9E) // Gray } Box( modifier = modifier diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/McpServersScreen.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/McpServersScreen.kt index f8980c5..ca6b9ed 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/McpServersScreen.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/McpServersScreen.kt @@ -1,7 +1,6 @@ package com.librechat.android.feature.settings.screen import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -46,17 +45,17 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.librechat.android.feature.settings.R import com.librechat.android.core.model.mcp.McpServer import com.librechat.android.core.model.mcp.McpServerStatus import com.librechat.android.core.ui.components.ErrorBanner import com.librechat.android.core.ui.components.LoadingIndicator +import com.librechat.android.feature.settings.R import com.librechat.android.feature.settings.viewmodel.McpViewModel +import org.koin.compose.viewmodel.koinViewModel /** Manages MCP server connections with status indicators, CRUD, and tool browsing. */ @OptIn(ExperimentalMaterial3Api::class) diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/McpSettingsSection.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/McpSettingsSection.kt index a521cc2..a03dc47 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/McpSettingsSection.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/McpSettingsSection.kt @@ -25,76 +25,78 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import com.librechat.android.feature.settings.R import com.librechat.android.core.model.mcp.McpServer import com.librechat.android.core.model.mcp.McpServerStatus +import com.librechat.android.feature.settings.R @Composable internal fun McpSettingsSection( servers: List, connectionStatus: Map, - reinitializingServers: Set = emptySet(), - error: String? = null, onAddServer: () -> Unit, onEditServer: (McpServer) -> Unit, onDeleteServer: (String) -> Unit, onReinitialize: (String) -> Unit, modifier: Modifier = Modifier, + reinitializingServers: Set = emptySet(), + error: String? = null, ) { - Column( - modifier = modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - if (error != null) { - Text( - text = stringResource(R.string.mcp_not_available), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } else if (servers.isEmpty()) { - Text( - text = stringResource(R.string.no_mcp_servers), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } else { - servers.forEach { server -> - McpServerItem( - server = server, - serverStatus = connectionStatus[server.name], - isReinitializing = server.name in reinitializingServers, - onEdit = { onEditServer(server) }, - onDelete = { onDeleteServer(server.name) }, - onReinitialize = { onReinitialize(server.name) }, + Column(modifier = modifier) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + if (error != null) { + Text( + text = stringResource(R.string.mcp_not_available), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, ) + } else if (servers.isEmpty()) { + Text( + text = stringResource(R.string.no_mcp_servers), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } else { + servers.forEach { server -> + McpServerItem( + server = server, + serverStatus = connectionStatus[server.name], + isReinitializing = server.name in reinitializingServers, + onEdit = { onEditServer(server) }, + onDelete = { onDeleteServer(server.name) }, + onReinitialize = { onReinitialize(server.name) }, + ) + } + } + + if (error == null) { + Spacer(modifier = Modifier.height(4.dp)) + } + + if (error == null) { + OutlinedButton( + onClick = onAddServer, + modifier = Modifier.fillMaxWidth(), + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(stringResource(R.string.add_mcp_server)) + } } } - - if (error == null) { - Spacer(modifier = Modifier.height(4.dp)) - } - - if (error == null) { - OutlinedButton( - onClick = onAddServer, - modifier = Modifier.fillMaxWidth(), - ) { - Icon( - imageVector = Icons.Default.Add, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(stringResource(R.string.add_mcp_server)) - } - } + HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) } - HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) } @Composable diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/McpToolList.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/McpToolList.kt index fe96f85..7edd81f 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/McpToolList.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/McpToolList.kt @@ -29,8 +29,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import com.librechat.android.feature.settings.R import com.librechat.android.core.model.mcp.McpTool +import com.librechat.android.feature.settings.R @Composable internal fun McpToolList( diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/McpToolsSheet.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/McpToolsSheet.kt index 2b19f1d..9e312cf 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/McpToolsSheet.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/McpToolsSheet.kt @@ -15,14 +15,13 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import com.librechat.android.feature.settings.R import com.librechat.android.core.model.mcp.McpTool +import com.librechat.android.feature.settings.R /** Bottom sheet listing MCP tools grouped by server; optional serverFilter narrows to one server. */ @OptIn(ExperimentalMaterial3Api::class) diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/MemoriesScreen.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/MemoriesScreen.kt index 74499a1..c6abf44 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/MemoriesScreen.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/MemoriesScreen.kt @@ -1,7 +1,6 @@ package com.librechat.android.feature.settings.screen import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -44,18 +43,18 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.librechat.android.feature.settings.R import com.librechat.android.core.model.Memory import com.librechat.android.core.ui.components.ErrorBanner import com.librechat.android.core.ui.components.LoadingIndicator +import com.librechat.android.feature.settings.R import com.librechat.android.feature.settings.viewmodel.MemoriesViewModel +import org.koin.compose.viewmodel.koinViewModel /** Full memory CRUD screen with PullToRefresh and dedicated MemoriesViewModel. */ @OptIn(ExperimentalMaterial3Api::class) diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/MemoriesSettingsSection.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/MemoriesSettingsSection.kt index 3c5d56b..04671fd 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/MemoriesSettingsSection.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/MemoriesSettingsSection.kt @@ -31,13 +31,13 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import com.librechat.android.feature.settings.R import com.librechat.android.core.model.Memory +import com.librechat.android.feature.settings.R @Composable internal fun MemoriesSettingsSection( @@ -53,85 +53,87 @@ internal fun MemoriesSettingsSection( onSaveMemory: (key: String, value: String) -> Unit, modifier: Modifier = Modifier, ) { - Column( - modifier = modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - // Enable/disable toggle - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, + Column(modifier = modifier) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), ) { - Column(modifier = Modifier.weight(1f)) { - Text( - text = stringResource(R.string.enable_memories), - style = MaterialTheme.typography.bodyLarge, + // Enable/disable toggle + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(R.string.enable_memories), + style = MaterialTheme.typography.bodyLarge, + ) + Text( + text = stringResource(R.string.enable_memories_desc), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Spacer(modifier = Modifier.width(16.dp)) + val toggleMemoriesCd = stringResource(R.string.cd_toggle_memories) + Switch( + checked = memoriesEnabled, + onCheckedChange = onToggleEnabled, + modifier = Modifier.semantics { + contentDescription = toggleMemoriesCd + }, ) + } + + Spacer(modifier = Modifier.height(4.dp)) + + // Memory list + if (memories.isEmpty()) { Text( - text = stringResource(R.string.enable_memories_desc), - style = MaterialTheme.typography.bodySmall, + text = stringResource(R.string.no_memories_saved), + style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) + } else { + memories.forEach { memory -> + MemoryItem( + memory = memory, + onEdit = { onEditMemory(memory) }, + onDelete = { onDeleteMemory(memory.key) }, + ) + } } - Spacer(modifier = Modifier.width(16.dp)) - val toggleMemoriesCd = stringResource(R.string.cd_toggle_memories) - Switch( - checked = memoriesEnabled, - onCheckedChange = onToggleEnabled, - modifier = Modifier.semantics { - contentDescription = toggleMemoriesCd - }, - ) - } - Spacer(modifier = Modifier.height(4.dp)) + Spacer(modifier = Modifier.height(4.dp)) - // Memory list - if (memories.isEmpty()) { - Text( - text = stringResource(R.string.no_memories_saved), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } else { - memories.forEach { memory -> - MemoryItem( - memory = memory, - onEdit = { onEditMemory(memory) }, - onDelete = { onDeleteMemory(memory.key) }, + OutlinedButton( + onClick = onAddMemory, + modifier = Modifier.fillMaxWidth(), + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = null, + modifier = Modifier.size(18.dp), ) + Spacer(modifier = Modifier.width(8.dp)) + Text(stringResource(R.string.add_memory)) } } + HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) - Spacer(modifier = Modifier.height(4.dp)) - - OutlinedButton( - onClick = onAddMemory, - modifier = Modifier.fillMaxWidth(), - ) { - Icon( - imageVector = Icons.Default.Add, - contentDescription = null, - modifier = Modifier.size(18.dp), + // Create/edit dialog + if (showMemoryDialog) { + MemoryDialog( + editingMemory = editingMemory, + onDismiss = onDismissDialog, + onSave = onSaveMemory, ) - Spacer(modifier = Modifier.width(8.dp)) - Text(stringResource(R.string.add_memory)) } - } - HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) - // Create/edit dialog - if (showMemoryDialog) { - MemoryDialog( - editingMemory = editingMemory, - onDismiss = onDismissDialog, - onSave = onSaveMemory, - ) + // Delete confirmation is handled inline via the delete icon } - - // Delete confirmation is handled inline via the delete icon } @Composable diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/MemoryEditDialog.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/MemoryEditDialog.kt index d9f462b..4d41fd1 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/MemoryEditDialog.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/MemoryEditDialog.kt @@ -16,8 +16,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import com.librechat.android.feature.settings.R import com.librechat.android.core.model.Memory +import com.librechat.android.feature.settings.R /** Add/edit memory dialog; key field is immutable when editing an existing memory. */ @Composable diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/PersonalizationDialog.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/PersonalizationDialog.kt index e696c66..a396dfa 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/PersonalizationDialog.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/PersonalizationDialog.kt @@ -21,8 +21,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import com.librechat.android.feature.settings.R import androidx.compose.ui.unit.dp +import com.librechat.android.feature.settings.R /** "About you" and "Response style" text areas with enable toggle; fields disabled when off. */ @Composable diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/PresetManagerScreen.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/PresetManagerScreen.kt index 55883db..46e5f13 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/PresetManagerScreen.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/PresetManagerScreen.kt @@ -1,7 +1,5 @@ package com.librechat.android.feature.settings.screen -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -9,7 +7,6 @@ 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 @@ -38,16 +35,16 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.librechat.android.feature.settings.R import com.librechat.android.core.ui.components.EmptyState import com.librechat.android.core.ui.components.LoadingIndicator import com.librechat.android.feature.settings.PresetManagerDisplayData +import com.librechat.android.feature.settings.R import com.librechat.android.feature.settings.viewmodel.PresetManagerViewModel +import org.koin.compose.viewmodel.koinViewModel /** Lists saved presets with delete-only management; editing reuses SavePresetDialog in chat. */ @OptIn(ExperimentalMaterial3Api::class) diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/SecuritySection.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/SecuritySection.kt index 814c2f1..df6577a 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/SecuritySection.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/SecuritySection.kt @@ -21,8 +21,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import com.librechat.android.feature.settings.R import androidx.compose.ui.unit.dp +import com.librechat.android.feature.settings.R @Composable internal fun SecuritySection( @@ -32,72 +32,74 @@ internal fun SecuritySection( onViewBackupCodes: () -> Unit, modifier: Modifier = Modifier, ) { - Column( - modifier = modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), - ) { - // 2FA status indicator - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, + Column(modifier = modifier) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), ) { - Icon( - imageVector = if (isTwoFactorEnabled) { - Icons.Default.CheckCircle - } else { - Icons.Default.Warning - }, - contentDescription = null, - modifier = Modifier.size(20.dp), - tint = if (isTwoFactorEnabled) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, - ) - Spacer(modifier = Modifier.width(12.dp)) - Column { - Text( - text = stringResource(R.string.two_factor_auth), - style = MaterialTheme.typography.bodyLarge, - ) - Text( - text = stringResource(if (isTwoFactorEnabled) R.string.status_enabled else R.string.status_disabled), - style = MaterialTheme.typography.bodySmall, - color = if (isTwoFactorEnabled) { + // 2FA status indicator + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = if (isTwoFactorEnabled) { + Icons.Default.CheckCircle + } else { + Icons.Default.Warning + }, + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = if (isTwoFactorEnabled) { MaterialTheme.colorScheme.primary } else { MaterialTheme.colorScheme.onSurfaceVariant }, ) + Spacer(modifier = Modifier.width(12.dp)) + Column { + Text( + text = stringResource(R.string.two_factor_auth), + style = MaterialTheme.typography.bodyLarge, + ) + Text( + text = stringResource(if (isTwoFactorEnabled) R.string.status_enabled else R.string.status_disabled), + style = MaterialTheme.typography.bodySmall, + color = if (isTwoFactorEnabled) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + ) + } } - } - // Enable/Disable 2FA button - OutlinedButton( - onClick = onToggleTwoFactor, - enabled = !isLoading, - modifier = Modifier.fillMaxWidth(), - ) { - Text( - stringResource(if (isTwoFactorEnabled) R.string.disable_two_factor else R.string.enable_two_factor), - ) - } - - // View backup codes (only when 2FA is enabled) - if (isTwoFactorEnabled) { + // Enable/Disable 2FA button OutlinedButton( - onClick = onViewBackupCodes, + onClick = onToggleTwoFactor, enabled = !isLoading, modifier = Modifier.fillMaxWidth(), ) { - Text(stringResource(R.string.view_backup_codes)) + Text( + stringResource(if (isTwoFactorEnabled) R.string.disable_two_factor else R.string.enable_two_factor), + ) } - } - Spacer(modifier = Modifier.height(0.dp)) + // View backup codes (only when 2FA is enabled) + if (isTwoFactorEnabled) { + OutlinedButton( + onClick = onViewBackupCodes, + enabled = !isLoading, + modifier = Modifier.fillMaxWidth(), + ) { + Text(stringResource(R.string.view_backup_codes)) + } + } + + Spacer(modifier = Modifier.height(0.dp)) + } + HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) } - HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) } diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/SettingsScreen.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/SettingsScreen.kt index 4e8aede..26474ab 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/SettingsScreen.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/SettingsScreen.kt @@ -36,7 +36,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp -import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.librechat.android.core.ui.components.ErrorBanner import com.librechat.android.core.ui.components.LoadingIndicator @@ -54,6 +53,7 @@ import com.librechat.android.feature.settings.screen.sections.ThemeSelector import com.librechat.android.feature.settings.screen.sections.TwoFactorCodeDialog import com.librechat.android.feature.settings.screen.sections.TwoFactorSetupDialog import com.librechat.android.feature.settings.viewmodel.SettingsViewModel +import org.koin.compose.viewmodel.koinViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -61,9 +61,9 @@ fun SettingsScreen( onLogout: () -> Unit, onNavigateBack: () -> Unit, onNavigateToArchived: () -> Unit, + modifier: Modifier = Modifier, onNavigateToSharedLinks: () -> Unit = {}, onNavigateToApiKeys: () -> Unit = {}, - modifier: Modifier = Modifier, viewModel: SettingsViewModel = koinViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -225,7 +225,11 @@ fun SettingsScreen( SettingsRow( icon = Icons.Default.Person, title = stringResource(R.string.personalization), - subtitle = if (uiState.personalizationEnabled) stringResource(R.string.status_enabled) else stringResource(R.string.status_disabled), + subtitle = if (uiState.personalizationEnabled) { + stringResource(R.string.status_enabled) + } else { + stringResource(R.string.status_disabled) + }, onClick = viewModel::showPersonalizationDialog, ) } diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/SharedLinksScreen.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/SharedLinksScreen.kt index 6bf2a74..4847f7f 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/SharedLinksScreen.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/SharedLinksScreen.kt @@ -47,12 +47,12 @@ import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import com.librechat.android.core.ui.components.EmptyState import com.librechat.android.feature.settings.R import com.librechat.android.feature.settings.SharedLinkDisplayData -import com.librechat.android.core.ui.components.EmptyState @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/SpeechDetailDialogs.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/SpeechDetailDialogs.kt index fd58778..b259485 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/SpeechDetailDialogs.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/SpeechDetailDialogs.kt @@ -15,8 +15,8 @@ import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuDefaults -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MenuAnchorType @@ -33,9 +33,9 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.librechat.android.feature.settings.R @@ -173,7 +173,6 @@ internal fun SttDetailDialog( } } } - } }, confirmButton = { @@ -207,12 +206,13 @@ internal fun TtsDetailDialog( availableEngines: List, availableVoices: List, availableDeviceVoices: List, + onConfirm: + (engine: String, voice: String, rate: Float, pitch: Float, deviceVoiceName: String, caching: Boolean, source: String) -> Unit, + onDismiss: () -> Unit, isPreviewPlaying: Boolean = false, onPreviewDevice: (text: String, rate: Float, pitch: Float, voiceName: String?) -> Unit = { _, _, _, _ -> }, onPreviewServer: (text: String, voice: String?, model: String?) -> Unit = { _, _, _ -> }, onStopPreview: () -> Unit = {}, - onConfirm: (engine: String, voice: String, rate: Float, pitch: Float, deviceVoiceName: String, caching: Boolean, source: String) -> Unit, - onDismiss: () -> Unit, ) { var engine by remember { mutableStateOf(selectedEngine) } var voice by remember { mutableStateOf(selectedVoice) } @@ -455,7 +455,8 @@ internal fun TtsDetailDialog( // Preview button val previewText = stringResource(R.string.tts_preview_text) - val previewCd = stringResource(if (isPreviewPlaying) R.string.cd_stop_voice_preview else R.string.cd_preview_voice) + val previewCd = + stringResource(if (isPreviewPlaying) R.string.cd_stop_voice_preview else R.string.cd_preview_voice) FilledTonalButton( onClick = { if (isPreviewPlaying) { diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/SpeechSettingsSection.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/SpeechSettingsSection.kt index d28d460..1765aa7 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/SpeechSettingsSection.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/SpeechSettingsSection.kt @@ -25,12 +25,12 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import com.librechat.android.feature.settings.R import com.librechat.android.core.model.speech.TtsVoice +import com.librechat.android.feature.settings.R @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -46,154 +46,156 @@ internal fun SpeechSettingsSection( onTestVoice: () -> Unit, modifier: Modifier = Modifier, ) { - Column( - modifier = modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - ) { - // Auto-send after speech toggle - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, + Column(modifier = modifier) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), ) { - Column(modifier = Modifier.weight(1f)) { - Text( - text = stringResource(R.string.auto_send_after_speech), - style = MaterialTheme.typography.bodyLarge, - ) - Text( - text = stringResource(R.string.auto_send_after_speech_desc), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - Spacer(modifier = Modifier.width(16.dp)) - val toggleAutoSendCd = stringResource(R.string.cd_toggle_auto_send_stt) - Switch( - checked = autoSendAfterSttEnabled, - onCheckedChange = onAutoSendAfterSttChange, - modifier = Modifier.semantics { - contentDescription = toggleAutoSendCd - }, - ) - } - - Spacer(modifier = Modifier.height(16.dp)) - - // Auto-read toggle - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - ) { - Column(modifier = Modifier.weight(1f)) { - Text( - text = stringResource(R.string.auto_read_responses), - style = MaterialTheme.typography.bodyLarge, - ) - Text( - text = stringResource(R.string.auto_read_responses_desc), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - Spacer(modifier = Modifier.width(16.dp)) - val toggleAutoReadCd = stringResource(R.string.cd_toggle_auto_read) - Switch( - checked = autoReadEnabled, - onCheckedChange = onAutoReadChange, - modifier = Modifier.semantics { - contentDescription = toggleAutoReadCd - }, - ) - } - - Spacer(modifier = Modifier.height(16.dp)) - - // TTS Voice selector (only relevant for server TTS) - if (ttsSource == "server") { - if (availableVoices.isNotEmpty()) { - Text( - text = stringResource(R.string.tts_voice), - style = MaterialTheme.typography.bodyLarge, - ) - Spacer(modifier = Modifier.height(8.dp)) - - var expanded by remember { mutableStateOf(false) } - - ExposedDropdownMenuBox( - expanded = expanded, - onExpandedChange = { expanded = it }, - ) { - val voiceName = selectedVoice?.name ?: stringResource(R.string.stt_default) - val voiceCd = stringResource(R.string.cd_tts_voice_selector, voiceName) - OutlinedTextField( - value = voiceName, - onValueChange = {}, - readOnly = true, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, - modifier = Modifier - .fillMaxWidth() - .menuAnchor(MenuAnchorType.PrimaryNotEditable) - .semantics { - contentDescription = voiceCd - }, + // Auto-send after speech toggle + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(R.string.auto_send_after_speech), + style = MaterialTheme.typography.bodyLarge, ) - ExposedDropdownMenu( + Text( + text = stringResource(R.string.auto_send_after_speech_desc), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Spacer(modifier = Modifier.width(16.dp)) + val toggleAutoSendCd = stringResource(R.string.cd_toggle_auto_send_stt) + Switch( + checked = autoSendAfterSttEnabled, + onCheckedChange = onAutoSendAfterSttChange, + modifier = Modifier.semantics { + contentDescription = toggleAutoSendCd + }, + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Auto-read toggle + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(R.string.auto_read_responses), + style = MaterialTheme.typography.bodyLarge, + ) + Text( + text = stringResource(R.string.auto_read_responses_desc), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Spacer(modifier = Modifier.width(16.dp)) + val toggleAutoReadCd = stringResource(R.string.cd_toggle_auto_read) + Switch( + checked = autoReadEnabled, + onCheckedChange = onAutoReadChange, + modifier = Modifier.semantics { + contentDescription = toggleAutoReadCd + }, + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + // TTS Voice selector (only relevant for server TTS) + if (ttsSource == "server") { + if (availableVoices.isNotEmpty()) { + Text( + text = stringResource(R.string.tts_voice), + style = MaterialTheme.typography.bodyLarge, + ) + Spacer(modifier = Modifier.height(8.dp)) + + var expanded by remember { mutableStateOf(false) } + + ExposedDropdownMenuBox( expanded = expanded, - onDismissRequest = { expanded = false }, + onExpandedChange = { expanded = it }, ) { - availableVoices.forEach { voice -> - DropdownMenuItem( - text = { - Column { - Text( - text = voice.name, - style = MaterialTheme.typography.bodyMedium, - ) - voice.provider?.let { provider -> + val voiceName = selectedVoice?.name ?: stringResource(R.string.stt_default) + val voiceCd = stringResource(R.string.cd_tts_voice_selector, voiceName) + OutlinedTextField( + value = voiceName, + onValueChange = {}, + readOnly = true, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + modifier = Modifier + .fillMaxWidth() + .menuAnchor(MenuAnchorType.PrimaryNotEditable) + .semantics { + contentDescription = voiceCd + }, + ) + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + ) { + availableVoices.forEach { voice -> + DropdownMenuItem( + text = { + Column { Text( - text = provider, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, + text = voice.name, + style = MaterialTheme.typography.bodyMedium, ) + voice.provider?.let { provider -> + Text( + text = provider, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } } - } - }, - onClick = { - onVoiceSelected(voice) - expanded = false - }, - ) + }, + onClick = { + onVoiceSelected(voice) + expanded = false + }, + ) + } } } - } - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(12.dp)) - // Test voice button - val testVoiceCd = stringResource(R.string.cd_test_tts_voice) - Button( - onClick = onTestVoice, - modifier = Modifier.semantics { - contentDescription = testVoiceCd - }, - ) { - Text(stringResource(R.string.test_voice)) + // Test voice button + val testVoiceCd = stringResource(R.string.cd_test_tts_voice) + Button( + onClick = onTestVoice, + modifier = Modifier.semantics { + contentDescription = testVoiceCd + }, + ) { + Text(stringResource(R.string.test_voice)) + } + } else { + Text( + text = stringResource(R.string.no_server_tts_voices), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) } } else { Text( - text = stringResource(R.string.no_server_tts_voices), + text = stringResource(R.string.using_device_tts), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) } - } else { - Text( - text = stringResource(R.string.using_device_tts), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) } + HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) } - HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) } diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/TabbedSettingsScreen.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/TabbedSettingsScreen.kt index 0b5b66f..fe1ecad 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/TabbedSettingsScreen.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/TabbedSettingsScreen.kt @@ -23,10 +23,10 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import org.koin.compose.viewmodel.koinViewModel import com.librechat.android.feature.settings.R import com.librechat.android.feature.settings.viewmodel.SettingsViewModel import kotlinx.coroutines.launch +import org.koin.compose.viewmodel.koinViewModel private val SettingsTabs = listOf("General", "Chat", "Account", "Data") diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/sections/AccountSection.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/sections/AccountSection.kt index e99aad0..f201331 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/sections/AccountSection.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/sections/AccountSection.kt @@ -35,62 +35,64 @@ internal fun AccountInfo( avatarUrl: String? = null, onAvatarClick: () -> Unit = {}, ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Surface( + Column { + Row( modifier = Modifier - .size(48.dp) - .clip(CircleShape), - color = MaterialTheme.colorScheme.primaryContainer, - onClick = onAvatarClick, + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, ) { - if (avatarUrl != null) { - AsyncImage( - model = avatarUrl, - contentDescription = stringResource(R.string.cd_user_avatar), - modifier = Modifier.size(48.dp), - contentScale = ContentScale.Crop, - ) - } else { + Surface( + modifier = Modifier + .size(48.dp) + .clip(CircleShape), + color = MaterialTheme.colorScheme.primaryContainer, + onClick = onAvatarClick, + ) { + if (avatarUrl != null) { + AsyncImage( + model = avatarUrl, + contentDescription = stringResource(R.string.cd_user_avatar), + modifier = Modifier.size(48.dp), + contentScale = ContentScale.Crop, + ) + } else { + Icon( + imageVector = Icons.Default.Person, + contentDescription = null, + modifier = Modifier.padding(12.dp), + tint = MaterialTheme.colorScheme.onPrimaryContainer, + ) + } + } + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + if (name.isNotBlank()) { + Text( + text = name, + style = MaterialTheme.typography.bodyLarge, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + if (email.isNotBlank()) { + Text( + text = email, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + IconButton(onClick = onAvatarClick) { Icon( - imageVector = Icons.Default.Person, - contentDescription = null, - modifier = Modifier.padding(12.dp), - tint = MaterialTheme.colorScheme.onPrimaryContainer, + imageVector = Icons.Default.CameraAlt, + contentDescription = stringResource(R.string.cd_change_avatar), + modifier = Modifier.size(20.dp), ) } } - Spacer(modifier = Modifier.width(16.dp)) - Column(modifier = Modifier.weight(1f)) { - if (name.isNotBlank()) { - Text( - text = name, - style = MaterialTheme.typography.bodyLarge, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - if (email.isNotBlank()) { - Text( - text = email, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - } - IconButton(onClick = onAvatarClick) { - Icon( - imageVector = Icons.Default.CameraAlt, - contentDescription = stringResource(R.string.cd_change_avatar), - modifier = Modifier.size(20.dp), - ) - } + HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) } - HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) } diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/sections/AppearanceSection.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/sections/AppearanceSection.kt index f526f77..255698e 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/sections/AppearanceSection.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/sections/AppearanceSection.kt @@ -32,37 +32,39 @@ internal fun ThemeSelector( selected: ThemeMode, onSelect: (ThemeMode) -> Unit, ) { - Column(modifier = Modifier.padding(horizontal = 16.dp)) { - ThemeMode.entries.forEach { mode -> - Row( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(8.dp)) - .selectable( + Column { + Column(modifier = Modifier.padding(horizontal = 16.dp)) { + ThemeMode.entries.forEach { mode -> + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .selectable( + selected = selected == mode, + onClick = { onSelect(mode) }, + role = Role.RadioButton, + ) + .padding(vertical = 8.dp, horizontal = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + RadioButton( selected = selected == mode, - onClick = { onSelect(mode) }, - role = Role.RadioButton, + onClick = null, ) - .padding(vertical = 8.dp, horizontal = 4.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - RadioButton( - selected = selected == mode, - onClick = null, - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = when (mode) { - ThemeMode.SYSTEM -> stringResource(R.string.theme_system) - ThemeMode.LIGHT -> stringResource(R.string.theme_light) - ThemeMode.DARK -> stringResource(R.string.theme_dark) - }, - style = MaterialTheme.typography.bodyLarge, - ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = when (mode) { + ThemeMode.SYSTEM -> stringResource(R.string.theme_system) + ThemeMode.LIGHT -> stringResource(R.string.theme_light) + ThemeMode.DARK -> stringResource(R.string.theme_dark) + }, + style = MaterialTheme.typography.bodyLarge, + ) + } } } - } - HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) + HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) + } // Column } @Composable @@ -71,39 +73,41 @@ internal fun TabletSidebarGestureToggle( onGestureEnabledChange: (Boolean) -> Unit, modifier: Modifier = Modifier, ) { - Column( - modifier = modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, + Column(modifier = modifier) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), ) { - Icon( - imageVector = Icons.Default.Tablet, - contentDescription = null, - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - Spacer(modifier = Modifier.width(16.dp)) - Column(modifier = Modifier.weight(1f)) { - Text( - text = stringResource(R.string.sidebar_swipe_gesture), - style = MaterialTheme.typography.bodyLarge, + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Default.Tablet, + contentDescription = null, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, ) - Text( - text = stringResource(R.string.sidebar_swipe_gesture_desc), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(R.string.sidebar_swipe_gesture), + style = MaterialTheme.typography.bodyLarge, + ) + Text( + text = stringResource(R.string.sidebar_swipe_gesture_desc), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Spacer(modifier = Modifier.width(16.dp)) + Switch( + checked = gestureEnabled, + onCheckedChange = onGestureEnabledChange, ) } - Spacer(modifier = Modifier.width(16.dp)) - Switch( - checked = gestureEnabled, - onCheckedChange = onGestureEnabledChange, - ) } + HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) } - HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) } diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/sections/ChatPreferencesSection.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/sections/ChatPreferencesSection.kt index c4d78f9..6cf5352 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/sections/ChatPreferencesSection.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/sections/ChatPreferencesSection.kt @@ -29,45 +29,47 @@ import com.librechat.android.feature.settings.R @Composable internal fun AboutInfo(serverUrl: String) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, + Column { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), ) { - Text( - text = stringResource(R.string.app_version_label), - style = MaterialTheme.typography.bodyMedium, - ) - Text( - text = stringResource(R.string.app_version_value), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - Spacer(modifier = Modifier.height(8.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = stringResource(R.string.server_label), - style = MaterialTheme.typography.bodyMedium, - ) - Text( - text = serverUrl.ifBlank { stringResource(R.string.server_not_configured) }, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.weight(1f, fill = false), - ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = stringResource(R.string.app_version_label), + style = MaterialTheme.typography.bodyMedium, + ) + Text( + text = stringResource(R.string.app_version_value), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = stringResource(R.string.server_label), + style = MaterialTheme.typography.bodyMedium, + ) + Text( + text = serverUrl.ifBlank { stringResource(R.string.server_not_configured) }, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f, fill = false), + ) + } } + HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) } - HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) } @Composable @@ -111,41 +113,43 @@ internal fun SettingsRow( onClick: () -> Unit, modifier: Modifier = Modifier, ) { - Surface( - modifier = modifier.fillMaxWidth(), - onClick = onClick, - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 12.dp), - verticalAlignment = Alignment.CenterVertically, + Column(modifier = modifier) { + Surface( + modifier = Modifier.fillMaxWidth(), + onClick = onClick, ) { - Icon( - imageVector = icon, - contentDescription = null, - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - Spacer(modifier = Modifier.width(16.dp)) - Column(modifier = Modifier.weight(1f)) { - Text( - text = title, - style = MaterialTheme.typography.bodyLarge, + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, ) - Text( - text = subtitle, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.bodyLarge, + ) + Text( + text = subtitle, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, ) } - Icon( - imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - contentDescription = null, - modifier = Modifier.size(20.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) } + HorizontalDivider() } - HorizontalDivider() } diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/sections/DataManagementSection.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/sections/DataManagementSection.kt index 2e0b518..a872158 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/sections/DataManagementSection.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/sections/DataManagementSection.kt @@ -29,51 +29,53 @@ internal fun DataExtraActions( onRevokeKeysClick: () -> Unit, isKeyRevoking: Boolean, ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 4.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - // Shared Links - OutlinedButton( - onClick = onSharedLinksClick, - modifier = Modifier.fillMaxWidth(), + Column { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 4.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), ) { - Row( + // Shared Links + OutlinedButton( + onClick = onSharedLinksClick, modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, ) { - Text(stringResource(R.string.shared_links)) - Icon( - imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text(stringResource(R.string.shared_links)) + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + } + } + + // Clear cache + OutlinedButton( + onClick = onClearCacheClick, + enabled = !isCacheClearing, + modifier = Modifier.fillMaxWidth(), + ) { + Text(stringResource(if (isCacheClearing) R.string.clearing else R.string.clear_cache)) + } + + // Revoke API keys + OutlinedButton( + onClick = onRevokeKeysClick, + enabled = !isKeyRevoking, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MaterialTheme.colorScheme.error, + ), + ) { + Text(stringResource(if (isKeyRevoking) R.string.revoking else R.string.revoke_all_api_keys)) } } - - // Clear cache - OutlinedButton( - onClick = onClearCacheClick, - enabled = !isCacheClearing, - modifier = Modifier.fillMaxWidth(), - ) { - Text(stringResource(if (isCacheClearing) R.string.clearing else R.string.clear_cache)) - } - - // Revoke API keys - OutlinedButton( - onClick = onRevokeKeysClick, - enabled = !isKeyRevoking, - modifier = Modifier.fillMaxWidth(), - colors = ButtonDefaults.outlinedButtonColors( - contentColor = MaterialTheme.colorScheme.error, - ), - ) { - Text(stringResource(if (isKeyRevoking) R.string.revoking else R.string.revoke_all_api_keys)) - } + HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) } - HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) } diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/sections/SecuritySection.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/sections/SecuritySection.kt index 413bb44..189ebb3 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/sections/SecuritySection.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/sections/SecuritySection.kt @@ -83,7 +83,6 @@ internal fun TwoFactorSetupDialog( ) } - @Composable internal fun TwoFactorCodeDialog( title: String, diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/sections/SpeechSection.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/sections/SpeechSection.kt index c25fa29..4f4c240 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/sections/SpeechSection.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/sections/SpeechSection.kt @@ -24,46 +24,48 @@ internal fun SpeechDetailButtons( onSttDetailClick: () -> Unit, onTtsDetailClick: () -> Unit, ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 4.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - OutlinedButton( - onClick = onSttDetailClick, - modifier = Modifier.fillMaxWidth(), + Column { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 4.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), ) { - Row( + OutlinedButton( + onClick = onSttDetailClick, modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, ) { - Text(stringResource(R.string.speech_to_text_settings)) - Icon( - imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - } - } - OutlinedButton( - onClick = onTtsDetailClick, - modifier = Modifier.fillMaxWidth(), - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text(stringResource(R.string.text_to_speech_settings)) - Icon( - imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text(stringResource(R.string.speech_to_text_settings)) + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + } + } + OutlinedButton( + onClick = onTtsDetailClick, + modifier = Modifier.fillMaxWidth(), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text(stringResource(R.string.text_to_speech_settings)) + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + } } } + HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) } - HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) } diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/McpViewModel.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/McpViewModel.kt index 1b9fed1..396b38e 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/McpViewModel.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/McpViewModel.kt @@ -15,7 +15,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import android.util.Log +import timber.log.Timber @Immutable data class McpUiState( @@ -76,7 +76,7 @@ class McpViewModel( _uiState.value = _uiState.value.copy(connectionStatus = result.data) } is Result.Error -> { - Log.d("McpViewModel", "Failed to load connection status: ${result.message}", result.exception) + Timber.d(result.exception, "Failed to load connection status: ${result.message}") } is Result.Loading -> { /* no-op */ } } @@ -90,7 +90,7 @@ class McpViewModel( _uiState.value = _uiState.value.copy(tools = result.data) } is Result.Error -> { - Log.d("McpViewModel", "Failed to load tools: ${result.message}", result.exception) + Timber.d(result.exception, "Failed to load tools: ${result.message}") } is Result.Loading -> { /* no-op */ } } diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/SettingsViewModel.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/SettingsViewModel.kt index 2c443df..08fff39 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/SettingsViewModel.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/SettingsViewModel.kt @@ -1,8 +1,7 @@ package com.librechat.android.feature.settings.viewmodel -import android.content.Context +import android.app.Application import android.net.Uri -import timber.log.Timber import androidx.compose.runtime.Immutable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -15,26 +14,22 @@ import com.librechat.android.core.data.datastore.SettingsDataStore import com.librechat.android.core.data.datastore.ThemeDataStore import com.librechat.android.core.data.datastore.ThemeMode import com.librechat.android.core.data.repository.AuthRepository +import com.librechat.android.core.data.repository.BalanceRepository import com.librechat.android.core.data.repository.ConversationRepository +import com.librechat.android.core.data.repository.KeyRepository import com.librechat.android.core.data.repository.McpRepository import com.librechat.android.core.data.repository.MemoryRepository +import com.librechat.android.core.data.repository.ShareRepository import com.librechat.android.core.data.repository.SpeechRepository import com.librechat.android.core.data.repository.UserRepository import com.librechat.android.core.model.Memory -import com.librechat.android.core.model.SharedLink import com.librechat.android.core.model.User import com.librechat.android.core.model.mcp.McpApiKeyConfig import com.librechat.android.core.model.mcp.McpOAuthConfig import com.librechat.android.core.model.mcp.McpServer import com.librechat.android.core.model.mcp.McpServerStatus import com.librechat.android.core.model.mcp.McpServerType -import com.librechat.android.core.model.request.CreateMemoryRequest -import com.librechat.android.core.model.request.UpdateMemoryPreferencesRequest -import com.librechat.android.core.model.request.UpdateMemoryRequest import com.librechat.android.core.model.speech.TtsVoice -import com.librechat.android.core.data.repository.BalanceRepository -import com.librechat.android.core.data.repository.KeyRepository -import com.librechat.android.core.data.repository.ShareRepository import com.librechat.android.feature.settings.SharedLinkDisplayData import com.librechat.android.feature.settings.UserDisplayData import com.librechat.android.feature.settings.screen.DeviceVoiceInfo @@ -51,6 +46,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import timber.log.Timber data class SettingsCommand( val name: String, @@ -186,7 +182,7 @@ private data class DataStorePreferences( ) class SettingsViewModel( - private val context: Context, + private val context: Application, private val userRepository: UserRepository, private val authRepository: AuthRepository, conversationRepository: ConversationRepository, @@ -210,7 +206,8 @@ class SettingsViewModel( private val memoryDelegate = MemoryManagementDelegate(stateHandle, memoryRepository) private val mcpDelegate = McpServerDelegate(stateHandle, mcpRepository) private val twoFactorDelegate = TwoFactorSecurityDelegate(stateHandle, authRepository) - private val dataDelegate = DataManagementDelegate(stateHandle, context, conversationRepository, shareRepository, keyRepository) + private val dataDelegate = + DataManagementDelegate(stateHandle, context, conversationRepository, shareRepository, keyRepository) private val speechDelegate = SpeechSettingsDelegate(stateHandle, context, speechRepository, settingsDataStore) /** Combined DataStore preferences flow. */ @@ -646,15 +643,22 @@ class SettingsViewModel( fun showAddMcpServerDialog() = mcpDelegate.showAddMcpServerDialog() fun showEditMcpServerDialog(server: McpServer) = mcpDelegate.showEditMcpServerDialog(server) fun dismissMcpServerDialog() = mcpDelegate.dismissMcpServerDialog() - fun saveMcpServer(name: String, description: String? = null, url: String, type: McpServerType, apiKey: McpApiKeyConfig? = null, oauth: McpOAuthConfig? = null) = - mcpDelegate.saveMcpServer(name, description, url, type, apiKey, oauth) + fun saveMcpServer( + name: String, + description: String? = null, + url: String, + type: McpServerType, + apiKey: McpApiKeyConfig? = null, + oauth: McpOAuthConfig? = null, + ) = mcpDelegate.saveMcpServer(name, description, url, type, apiKey, oauth) fun deleteMcpServer(serverName: String) = mcpDelegate.deleteMcpServer(serverName) fun reinitializeMcpServer(serverName: String) = mcpDelegate.reinitializeMcpServer(serverName) fun dismissMcpReinitializeMessage() = mcpDelegate.dismissMcpReinitializeMessage() // Two-factor security fun toggleTwoFactor() = twoFactorDelegate.toggleTwoFactor() - fun enableTwoFactorWithOtp(token: String?, backupCode: String?) = twoFactorDelegate.enableTwoFactor(token = token, backupCode = backupCode) + fun enableTwoFactorWithOtp(token: String?, backupCode: String?) = + twoFactorDelegate.enableTwoFactor(token = token, backupCode = backupCode) fun dismissEnableTwoFactorOtpDialog() = twoFactorDelegate.dismissEnableTwoFactorOtpDialog() fun confirmEnableTwoFactor(code: String) = twoFactorDelegate.confirmEnableTwoFactor(code) fun confirmDisableTwoFactor(code: String) = twoFactorDelegate.confirmDisableTwoFactor(code) @@ -662,7 +666,8 @@ class SettingsViewModel( fun dismissDisableTwoFactorDialog() = twoFactorDelegate.dismissDisableTwoFactorDialog() fun dismissBackupCodesDialog() = twoFactorDelegate.dismissBackupCodesDialog() fun viewBackupCodes() = twoFactorDelegate.viewBackupCodes() - fun viewBackupCodesWithOtp(token: String?, backupCode: String?) = twoFactorDelegate.viewBackupCodes(token = token, backupCode = backupCode) + fun viewBackupCodesWithOtp(token: String?, backupCode: String?) = + twoFactorDelegate.viewBackupCodes(token = token, backupCode = backupCode) fun dismissBackupCodesOtpDialog() = twoFactorDelegate.dismissBackupCodesOtpDialog() // Data management @@ -681,16 +686,25 @@ class SettingsViewModel( fun setAutoReadEnabled(enabled: Boolean) = speechDelegate.setAutoReadEnabled(enabled) fun selectVoice(voice: TtsVoice) = speechDelegate.selectVoice(voice) fun testVoice() = speechDelegate.testVoice() - fun previewDeviceTts(text: String, rate: Float, pitch: Float, voiceName: String?) = speechDelegate.previewDeviceTts(text, rate, pitch, voiceName) - fun previewServerTts(text: String, voice: String?, model: String?) = speechDelegate.previewServerTts(text, voice, model) + fun previewDeviceTts(text: String, rate: Float, pitch: Float, voiceName: String?) = + speechDelegate.previewDeviceTts(text, rate, pitch, voiceName) + fun previewServerTts(text: String, voice: String?, model: String?) = + speechDelegate.previewServerTts(text, voice, model) fun stopTtsPreview() = speechDelegate.stopTtsPreview() fun showSttDetailDialog() = speechDelegate.showSttDetailDialog() fun dismissSttDetailDialog() = speechDelegate.dismissSttDetailDialog() fun saveSttSettings(engine: String, language: String) = speechDelegate.saveSttSettings(engine, language) fun showTtsDetailDialog() = speechDelegate.showTtsDetailDialog() fun dismissTtsDetailDialog() = speechDelegate.dismissTtsDetailDialog() - fun saveTtsSettings(engine: String, voice: String, rate: Float, pitch: Float, deviceVoiceName: String, caching: Boolean, source: String) = - speechDelegate.saveTtsSettings(engine, voice, rate, pitch, deviceVoiceName, caching, source) + fun saveTtsSettings( + engine: String, + voice: String, + rate: Float, + pitch: Float, + deviceVoiceName: String, + caching: Boolean, + source: String, + ) = speechDelegate.saveTtsSettings(engine, voice, rate, pitch, deviceVoiceName, caching, source) override fun onCleared() { super.onCleared() diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/delegate/DataManagementDelegate.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/delegate/DataManagementDelegate.kt index a22ecfa..64afe4a 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/delegate/DataManagementDelegate.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/delegate/DataManagementDelegate.kt @@ -1,7 +1,6 @@ package com.librechat.android.feature.settings.viewmodel.delegate import android.content.Context -import android.util.Log import com.librechat.android.core.common.result.Result import com.librechat.android.core.data.repository.ConversationRepository import com.librechat.android.core.data.repository.KeyRepository diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/delegate/McpServerDelegate.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/delegate/McpServerDelegate.kt index 6ba7862..8dbc698 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/delegate/McpServerDelegate.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/delegate/McpServerDelegate.kt @@ -1,6 +1,5 @@ package com.librechat.android.feature.settings.viewmodel.delegate -import android.util.Log import com.librechat.android.core.common.result.Result import com.librechat.android.core.data.repository.McpRepository import com.librechat.android.core.model.mcp.McpApiKeyConfig @@ -9,6 +8,7 @@ import com.librechat.android.core.model.mcp.McpServer import com.librechat.android.core.model.mcp.McpServerType import com.librechat.android.feature.settings.viewmodel.SettingsStateHandle import kotlinx.coroutines.launch +import timber.log.Timber /** * Handles MCP server management, connection status, and reinitialization. @@ -25,7 +25,7 @@ class McpServerDelegate( stateHandle.update { copy(mcpServers = result.data, mcpError = null) } } is Result.Error -> { - Log.d("SettingsViewModel", "Failed to load MCP servers: ${result.message}", result.exception) + Timber.d(result.exception, "Failed to load MCP servers: ${result.message}") stateHandle.update { copy(mcpError = result.message ?: "MCP not available on this server") } } is Result.Loading -> { /* no-op */ } @@ -37,7 +37,7 @@ class McpServerDelegate( stateHandle.update { copy(mcpConnectionStatus = result.data) } } is Result.Error -> { - Log.d("SettingsViewModel", "Failed to load MCP connection status: ${result.message}", result.exception) + Timber.d(result.exception, "Failed to load MCP connection status: ${result.message}") } is Result.Loading -> { /* no-op */ } } diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/delegate/MemoryManagementDelegate.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/delegate/MemoryManagementDelegate.kt index 17e5e6f..1248ef2 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/delegate/MemoryManagementDelegate.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/delegate/MemoryManagementDelegate.kt @@ -1,6 +1,5 @@ package com.librechat.android.feature.settings.viewmodel.delegate -import android.util.Log import com.librechat.android.core.common.result.Result import com.librechat.android.core.data.repository.MemoryRepository import com.librechat.android.core.model.Memory @@ -9,6 +8,7 @@ import com.librechat.android.core.model.request.UpdateMemoryPreferencesRequest import com.librechat.android.core.model.request.UpdateMemoryRequest import com.librechat.android.feature.settings.viewmodel.SettingsStateHandle import kotlinx.coroutines.launch +import timber.log.Timber /** * Handles memory CRUD operations and memory preferences. @@ -25,7 +25,7 @@ class MemoryManagementDelegate( stateHandle.update { copy(memories = result.data) } } is Result.Error -> { - Log.d("SettingsViewModel", "Failed to load memories: ${result.message}", result.exception) + Timber.d(result.exception, "Failed to load memories: ${result.message}") } is Result.Loading -> { /* no-op */ } } diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/delegate/SpeechSettingsDelegate.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/delegate/SpeechSettingsDelegate.kt index fa7b495..24a1050 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/delegate/SpeechSettingsDelegate.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/delegate/SpeechSettingsDelegate.kt @@ -3,7 +3,6 @@ package com.librechat.android.feature.settings.viewmodel.delegate import android.content.Context import android.media.MediaPlayer import android.speech.tts.TextToSpeech -import android.util.Log import com.librechat.android.core.common.result.Result import com.librechat.android.core.data.datastore.SettingsDataStore import com.librechat.android.core.data.repository.SpeechRepository @@ -11,6 +10,7 @@ import com.librechat.android.core.model.speech.TtsVoice import com.librechat.android.feature.settings.screen.DeviceVoiceInfo import com.librechat.android.feature.settings.viewmodel.SettingsStateHandle import kotlinx.coroutines.launch +import timber.log.Timber /** * Handles TTS voice selection, test playback, device voice loading, and MediaPlayer lifecycle. @@ -32,7 +32,7 @@ class SpeechSettingsDelegate( stateHandle.update { copy(availableVoices = result.data) } } is Result.Error -> { - Log.d("SettingsViewModel", "Failed to load voices: ${result.message}", result.exception) + Timber.d(result.exception, "Failed to load voices: ${result.message}") } is Result.Loading -> { /* no-op */ } } @@ -107,6 +107,7 @@ class SpeechSettingsDelegate( override fun onDone(utteranceId: String?) { stateHandle.update { copy(isTtsPreviewPlaying = false) } } + @Deprecated("Deprecated in Java") override fun onError(utteranceId: String?) { stateHandle.update { copy(isTtsPreviewPlaying = false) } @@ -176,7 +177,15 @@ class SpeechSettingsDelegate( stateHandle.update { copy(showTtsDetailDialog = false) } } - fun saveTtsSettings(engine: String, voice: String, rate: Float, pitch: Float, deviceVoiceName: String, caching: Boolean, source: String) { + fun saveTtsSettings( + engine: String, + voice: String, + rate: Float, + pitch: Float, + deviceVoiceName: String, + caching: Boolean, + source: String, + ) { stateHandle.update { copy( ttsEngine = engine, @@ -225,7 +234,7 @@ class SpeechSettingsDelegate( } } mediaPlayer.setOnErrorListener { mp, what, extra -> - Log.e("SettingsViewModel", "MediaPlayer error: what=$what, extra=$extra") + Timber.e("MediaPlayer error: what=$what, extra=$extra") mp.release() currentMediaPlayer = null tempFile.delete() @@ -246,7 +255,7 @@ class SpeechSettingsDelegate( currentMediaPlayer = mediaPlayer } catch (e: Exception) { - Log.e("SettingsViewModel", "Failed to play audio", e) + Timber.e(e, "Failed to play audio") stateHandle.update { copy( error = "Audio playback failed: ${e.localizedMessage}", diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/delegate/TwoFactorSecurityDelegate.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/delegate/TwoFactorSecurityDelegate.kt index 0c9eb0e..a3c0b34 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/delegate/TwoFactorSecurityDelegate.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/delegate/TwoFactorSecurityDelegate.kt @@ -163,7 +163,6 @@ class TwoFactorSecurityDelegate( fun dismissBackupCodesOtpDialog() { stateHandle.update { copy(showBackupCodesOtpDialog = false) } } - } /** diff --git a/feature/settings/src/main/res/values/strings.xml b/feature/settings/src/main/res/values/strings.xml index bbbf73d..71ea7f5 100644 --- a/feature/settings/src/main/res/values/strings.xml +++ b/feature/settings/src/main/res/values/strings.xml @@ -349,7 +349,7 @@ Key name e.g. My App API Key Created - Your API key has been created. Copy it now -- you won\'t be able to see it again. + Your API key has been created. Copy it now — you won\'t be able to see it again. Copied to clipboard diff --git a/feature/settings/src/test/kotlin/com/librechat/android/feature/settings/viewmodel/SettingsViewModelTest.kt b/feature/settings/src/test/kotlin/com/librechat/android/feature/settings/viewmodel/SettingsViewModelTest.kt index b7b0b80..2ce77dd 100644 --- a/feature/settings/src/test/kotlin/com/librechat/android/feature/settings/viewmodel/SettingsViewModelTest.kt +++ b/feature/settings/src/test/kotlin/com/librechat/android/feature/settings/viewmodel/SettingsViewModelTest.kt @@ -1,6 +1,6 @@ package com.librechat.android.feature.settings.viewmodel -import android.content.Context +import android.app.Application import com.google.common.truth.Truth.assertThat import com.librechat.android.core.common.ChatLayoutConstants import com.librechat.android.core.common.result.Result @@ -41,7 +41,7 @@ class SettingsViewModelTest { private val testDispatcher = StandardTestDispatcher() - private val context = mockk(relaxed = true) + private val context = mockk(relaxed = true) private val userRepository = mockk(relaxed = true) private val authRepository = mockk(relaxed = true) private val conversationRepository = mockk(relaxed = true) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 65361ca..661d581 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -50,8 +50,10 @@ truth = "1.4.4" espresso = "3.6.1" # Code Quality -detekt = "1.23.7" +detekt = "1.23.8" ktlint = "12.1.2" +compose-lint-checks = "1.4.2" +kover = "0.9.8" # Fuzzy Search fuzzywuzzy = "1.4.0" @@ -145,6 +147,14 @@ compose-gradlePlugin = { group = "org.jetbrains.kotlin", name = "compose-compile ksp-gradlePlugin = { group = "com.google.devtools.ksp", name = "symbol-processing-gradle-plugin", version.ref = "ksp" } room-gradlePlugin = { group = "androidx.room", name = "room-gradle-plugin", version.ref = "room" } +# Code Quality +detekt-formatting = { group = "io.gitlab.arturbosch.detekt", name = "detekt-formatting", version.ref = "detekt" } +detekt-koin = { group = "dev.androidbroadcast.rules.koin", name = "detekt-koin4-rules", version = "1.1.0" } +detekt-gradlePlugin = { group = "io.gitlab.arturbosch.detekt", name = "detekt-gradle-plugin", version.ref = "detekt" } +leakcanary = { group = "com.squareup.leakcanary", name = "leakcanary-android", version = "2.14" } +compose-lint-checks = { group = "com.slack.lint.compose", name = "compose-lint-checks", version.ref = "compose-lint-checks" } +kover-gradlePlugin = { group = "org.jetbrains.kotlinx", name = "kover-gradle-plugin", version.ref = "kover" } + # Fuzzy Search fuzzywuzzy = { group = "me.xdrop", name = "fuzzywuzzy", version.ref = "fuzzywuzzy" } @@ -204,3 +214,4 @@ ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } room = { id = "androidx.room", version.ref = "room" } detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } +kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } diff --git a/lint.xml b/lint.xml new file mode 100644 index 0000000..54e44c0 --- /dev/null +++ b/lint.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + +