Add linting and static code analysis

This commit is contained in:
Garfie 2026-03-28 12:49:37 -05:00
parent e4effcf40f
commit 1868bab690
215 changed files with 3158 additions and 2676 deletions

10
.github/dependabot.yml vendored Normal file
View file

@ -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.

View file

@ -39,6 +39,8 @@ dependencies {
implementation(libs.coil.compose) implementation(libs.coil.compose)
implementation(libs.timber) implementation(libs.timber)
debugImplementation(libs.leakcanary)
androidTestImplementation(libs.compose.ui.test) androidTestImplementation(libs.compose.ui.test)
debugImplementation(libs.compose.ui.test.manifest) debugImplementation(libs.compose.ui.test.manifest)
} }

View file

@ -23,14 +23,15 @@ import androidx.compose.material3.Text
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.SideEffect import androidx.compose.runtime.SideEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.librechat.android.core.common.network.ConnectivityObserver import com.librechat.android.core.common.network.ConnectivityObserver
import com.librechat.android.core.data.datastore.ThemeDataStore import com.librechat.android.core.data.datastore.ThemeDataStore
import com.librechat.android.core.data.datastore.ThemeMode 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 com.librechat.android.navigation.LibreChatNavHost
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import timber.log.Timber import timber.log.Timber
import androidx.compose.ui.res.stringResource
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@ -153,6 +153,7 @@ class MainActivity : ComponentActivity() {
private fun handleShareIntent(intent: Intent) { private fun handleShareIntent(intent: Intent) {
val sharedText = intent.getStringExtra(Intent.EXTRA_TEXT) val sharedText = intent.getStringExtra(Intent.EXTRA_TEXT)
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
val sharedUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val sharedUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java) intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)

View file

@ -48,16 +48,16 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.librechat.android.R
import com.librechat.android.core.model.EModelEndpoint import com.librechat.android.core.model.EModelEndpoint
import com.librechat.android.core.ui.components.isMonochromeIcon import com.librechat.android.core.ui.components.isMonochromeIcon
import com.librechat.android.core.ui.components.toIconRes 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. * Lightweight snapshot of fields DrawerConversationItem actually renders.
@ -119,10 +119,10 @@ fun DrawerContent(
onSettingsClick: () -> Unit, onSettingsClick: () -> Unit,
onAgentsClick: () -> Unit, onAgentsClick: () -> Unit,
onFilesClick: () -> Unit, onFilesClick: () -> Unit,
modifier: Modifier = Modifier,
onToggleFavorite: (String) -> Unit = {}, onToggleFavorite: (String) -> Unit = {},
onRefresh: () -> Unit = {}, onRefresh: () -> Unit = {},
onLoadMore: () -> Unit = {}, onLoadMore: () -> Unit = {},
modifier: Modifier = Modifier,
) { ) {
Column( Column(
modifier = modifier modifier = modifier
@ -367,8 +367,8 @@ fun DrawerContent(
private fun DrawerConversationItem( private fun DrawerConversationItem(
data: DrawerConversationDisplayData, data: DrawerConversationDisplayData,
onClick: () -> Unit, onClick: () -> Unit,
onToggleFavorite: () -> Unit = {},
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onToggleFavorite: () -> Unit = {},
) { ) {
val endpointIconRes = remember(data.endpoint) { val endpointIconRes = remember(data.endpoint) {
data.endpoint?.toIconRes() data.endpoint?.toIconRes()
@ -517,4 +517,3 @@ private fun DrawerFooterItem(
) )
} }
} }

View file

@ -1,8 +1,6 @@
package com.librechat.android.navigation package com.librechat.android.navigation
import android.net.Uri import android.net.Uri
import com.librechat.android.MainActivity
import timber.log.Timber
import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.LinearEasing 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.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.ui.unit.dp
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DrawerValue import androidx.compose.material3.DrawerValue
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -31,12 +28,16 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.IntOffset 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.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController 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.AGENT_EDITOR_CREATE_ROUTE
import com.librechat.android.feature.agents.navigation.agentsGraph import com.librechat.android.feature.agents.navigation.agentsGraph
import com.librechat.android.feature.auth.navigation.AUTH_GRAPH_ROUTE 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.files.navigation.filesGraph
import com.librechat.android.feature.settings.navigation.API_KEYS_ROUTE import com.librechat.android.feature.settings.navigation.API_KEYS_ROUTE
import com.librechat.android.feature.settings.navigation.PRESET_MANAGER_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_ACCOUNT_ROUTE
import com.librechat.android.feature.settings.navigation.SETTINGS_CHAT_ROUTE import com.librechat.android.feature.settings.navigation.SETTINGS_CHAT_ROUTE
import com.librechat.android.feature.settings.navigation.SETTINGS_DATA_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.SHARED_LINKS_ROUTE
import com.librechat.android.feature.settings.navigation.settingsGraph import com.librechat.android.feature.settings.navigation.settingsGraph
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import com.librechat.android.R import org.koin.compose.viewmodel.koinViewModel
import androidx.compose.ui.res.stringResource import timber.log.Timber
/** Maps a [SettingsCategory] to its corresponding navigation route. */ /** Maps a [SettingsCategory] to its corresponding navigation route. */
fun SettingsCategory.toRoute(): String = when (this) { fun SettingsCategory.toRoute(): String = when (this) {
@ -311,118 +311,118 @@ private fun PhoneLayout(
navController = navController, navController = navController,
startDestination = startDestination, startDestination = startDestination,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
enterTransition = { enterTransition = {
slideIntoContainer( slideIntoContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Start, towards = AnimatedContentTransitionScope.SlideDirection.Start,
animationSpec = tween(300), 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 }
}
},
) )
}, chatGraph(
exitTransition = { navController = navController,
fadeOut(animationSpec = tween(150)) onOpenDrawer = {
}, scope.launch { drawerState.open() }
popEnterTransition = { },
fadeIn(
initialAlpha = 0.5f,
animationSpec = tween(300, easing = LinearEasing),
) + scaleIn(
initialScale = 0.92f,
animationSpec = tween(300, easing = LinearEasing),
) )
}, conversationsGraph(
popExitTransition = { onConversationClick = { conversationId ->
slideOut( navController.navigateToChat(conversationId)
targetOffset = { IntOffset((it.width * 0.15f).toInt(), 0) }, },
animationSpec = tween(300, easing = FastOutSlowInEasing), onNavigateToArchived = {
) + scaleOut( navController.navigate("conversations/archived")
targetScale = 0.92f, },
animationSpec = tween(300, easing = LinearEasing), onNavigateBackFromArchived = {
navController.popBackStack()
},
) )
}, agentsGraph(
) { onAgentClick = { agentId ->
authGraph( navController.navigate("agents/$agentId")
navController = navController, },
onAuthComplete = { onBack = { navController.popBackStack() },
navHostViewModel.onAuthComplete() onStartChat = { agentId ->
navController.navigate(CHAT_GRAPH_ROUTE) { navController.navigate(NEW_CHAT_ROUTE) {
popUpTo(AUTH_GRAPH_ROUTE) { inclusive = true } popUpTo(CHAT_GRAPH_ROUTE) { inclusive = false }
} launchSingleTop = true
}, }
) },
chatGraph( onCreateAgent = {
navController = navController, navController.navigate(AGENT_EDITOR_CREATE_ROUTE)
onOpenDrawer = { },
scope.launch { drawerState.open() } onEditAgent = { agentId ->
}, navController.navigate("agents/editor/$agentId")
) },
conversationsGraph( )
onConversationClick = { conversationId -> filesGraph()
navController.navigateToChat(conversationId) settingsGraph(
}, onLogout = {
onNavigateToArchived = { navHostViewModel.logout()
navController.navigate("conversations/archived") navController.navigate(AUTH_GRAPH_ROUTE) {
}, popUpTo(0) { inclusive = true }
onNavigateBackFromArchived = { }
navController.popBackStack() },
}, onNavigateBack = { navController.popBackStack() },
) onNavigateToArchived = {
agentsGraph( navController.navigate("conversations/archived") {
onAgentClick = { agentId -> launchSingleTop = true
navController.navigate("agents/$agentId") }
}, },
onBack = { navController.popBackStack() }, onNavigateToSharedLinks = {
onStartChat = { agentId -> navController.navigate(SHARED_LINKS_ROUTE) {
navController.navigate(NEW_CHAT_ROUTE) { launchSingleTop = true
popUpTo(CHAT_GRAPH_ROUTE) { inclusive = false } }
launchSingleTop = true },
} onNavigateBackFromSharedLinks = {
}, navController.popBackStack()
onCreateAgent = { },
navController.navigate(AGENT_EDITOR_CREATE_ROUTE) onNavigateToPresets = {
}, navController.navigate(PRESET_MANAGER_ROUTE) {
onEditAgent = { agentId -> launchSingleTop = true
navController.navigate("agents/editor/$agentId") }
}, },
) onNavigateBackFromPresets = {
filesGraph() navController.popBackStack()
settingsGraph( },
onLogout = { onNavigateToApiKeys = {
navHostViewModel.logout() navController.navigate(API_KEYS_ROUTE) {
navController.navigate(AUTH_GRAPH_ROUTE) { launchSingleTop = true
popUpTo(0) { inclusive = true } }
} },
}, onNavigateBackFromApiKeys = {
onNavigateBack = { navController.popBackStack() }, 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()
},
)
}
} }
} }
} }

View file

@ -3,22 +3,16 @@ package com.librechat.android.navigation
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope 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.AuthRepository
import com.librechat.android.core.data.repository.BannerRepository import com.librechat.android.core.data.repository.BannerRepository
import com.librechat.android.core.data.repository.ConfigRepository import com.librechat.android.core.data.repository.ConfigRepository
import com.librechat.android.core.data.repository.ConversationRepository 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.Banner
import com.librechat.android.core.model.Conversation import com.librechat.android.core.model.Conversation
import com.librechat.android.core.model.EModelEndpoint
import com.librechat.android.core.network.client.TokenManager 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.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -32,6 +26,10 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import timber.log.Timber 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 * Combined UI state for the drawer sidebar. A single emission replaces
@ -155,7 +153,13 @@ class NavHostViewModel(
* not the entire PhoneLayout/TabletLayout. * not the entire PhoneLayout/TabletLayout.
*/ */
val drawerUiState: StateFlow<DrawerUiState> = combine( val drawerUiState: StateFlow<DrawerUiState> = 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) DrawerDataSnapshot(grouped, activeId, favIds, favConvos, query)
}, },
combine(_isRefreshing, _isLoadingMore, _hasMore) { refreshing, loadingMore, hasMore -> combine(_isRefreshing, _isLoadingMore, _hasMore) { refreshing, loadingMore, hasMore ->

View file

@ -27,11 +27,11 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.librechat.android.R import com.librechat.android.R
import androidx.compose.ui.res.stringResource
/** /**
* Settings sidebar content shown when the sidebar mode is [SidebarMode.Settings]. * Settings sidebar content shown when the sidebar mode is [SidebarMode.Settings].

View file

@ -1,8 +1,6 @@
package com.librechat.android.navigation package com.librechat.android.navigation
import android.net.Uri import android.net.Uri
import com.librechat.android.MainActivity
import timber.log.Timber
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.core.Animatable 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.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.ui.layout.Layout
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.VerticalDivider import androidx.compose.material3.VerticalDivider
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -35,12 +32,14 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.util.VelocityTracker import androidx.compose.ui.input.pointer.util.VelocityTracker
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import com.librechat.android.MainActivity
import com.librechat.android.core.ui.components.BannerDisplay 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.AGENT_EDITOR_CREATE_ROUTE
import com.librechat.android.feature.agents.navigation.agentsGraph 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.SHARED_LINKS_ROUTE
import com.librechat.android.feature.settings.navigation.settingsGraph import com.librechat.android.feature.settings.navigation.settingsGraph
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
private val SidebarWidth = 320.dp private val SidebarWidth = 320.dp
@ -73,8 +73,8 @@ fun TabletLayout(
isInAuthFlow: Boolean, isInAuthFlow: Boolean,
deepLinkUri: Uri?, deepLinkUri: Uri?,
onDeepLinkConsumed: () -> Unit, onDeepLinkConsumed: () -> Unit,
shareNavigationTrigger: Int = 0,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
shareNavigationTrigger: Int = 0,
) { ) {
// Banner state only -- drawer state is collected inside DrawerContent itself // Banner state only -- drawer state is collected inside DrawerContent itself
val banners by navHostViewModel.banners.collectAsStateWithLifecycle() val banners by navHostViewModel.banners.collectAsStateWithLifecycle()

View file

@ -8,6 +8,8 @@ dependencies {
compileOnly(libs.compose.gradlePlugin) compileOnly(libs.compose.gradlePlugin)
compileOnly(libs.ksp.gradlePlugin) compileOnly(libs.ksp.gradlePlugin)
compileOnly(libs.room.gradlePlugin) compileOnly(libs.room.gradlePlugin)
compileOnly(libs.detekt.gradlePlugin)
compileOnly(libs.kover.gradlePlugin)
} }
gradlePlugin { gradlePlugin {
@ -40,5 +42,9 @@ gradlePlugin {
id = "librechat.kotlin.serialization" id = "librechat.kotlin.serialization"
implementationClass = "KotlinSerializationConventionPlugin" implementationClass = "KotlinSerializationConventionPlugin"
} }
register("androidDetekt") {
id = "librechat.android.detekt"
implementationClass = "AndroidDetektConventionPlugin"
}
} }
} }

View file

@ -8,6 +8,8 @@ class AndroidApplicationConventionPlugin : Plugin<Project> {
with(target) { with(target) {
pluginManager.apply("com.android.application") pluginManager.apply("com.android.application")
pluginManager.apply("org.jetbrains.kotlin.android") pluginManager.apply("org.jetbrains.kotlin.android")
pluginManager.apply("librechat.android.detekt")
pluginManager.apply("org.jetbrains.kotlinx.kover")
extensions.configure<ApplicationExtension> { extensions.configure<ApplicationExtension> {
compileSdk = 35 compileSdk = 35
@ -23,6 +25,9 @@ class AndroidApplicationConventionPlugin : Plugin<Project> {
targetCompatibility = org.gradle.api.JavaVersion.VERSION_17 targetCompatibility = org.gradle.api.JavaVersion.VERSION_17
isCoreLibraryDesugaringEnabled = true isCoreLibraryDesugaringEnabled = true
} }
lint {
disable += "NullSafeMutableLiveData"
}
buildTypes { buildTypes {
release { release {
isMinifyEnabled = true isMinifyEnabled = true

View file

@ -49,6 +49,7 @@ class AndroidComposeConventionPlugin : Plugin<Project> {
add("debugImplementation", libs.findLibrary("compose-ui-tooling").get()) add("debugImplementation", libs.findLibrary("compose-ui-tooling").get())
add("androidTestImplementation", platform(bom)) add("androidTestImplementation", platform(bom))
add("androidTestImplementation", libs.findLibrary("compose-ui-test").get()) add("androidTestImplementation", libs.findLibrary("compose-ui-test").get())
add("lintChecks", libs.findLibrary("compose-lint-checks").get())
} }
} }
} }

View file

@ -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<Project> {
override fun apply(target: Project) {
with(target) {
pluginManager.apply("io.gitlab.arturbosch.detekt")
extensions.configure<DetektExtension> {
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")

View file

@ -8,6 +8,8 @@ class AndroidLibraryConventionPlugin : Plugin<Project> {
with(target) { with(target) {
pluginManager.apply("com.android.library") pluginManager.apply("com.android.library")
pluginManager.apply("org.jetbrains.kotlin.android") pluginManager.apply("org.jetbrains.kotlin.android")
pluginManager.apply("librechat.android.detekt")
pluginManager.apply("org.jetbrains.kotlinx.kover")
extensions.configure<LibraryExtension> { extensions.configure<LibraryExtension> {
compileSdk = 35 compileSdk = 35
@ -20,6 +22,11 @@ class AndroidLibraryConventionPlugin : Plugin<Project> {
targetCompatibility = org.gradle.api.JavaVersion.VERSION_17 targetCompatibility = org.gradle.api.JavaVersion.VERSION_17
isCoreLibraryDesugaringEnabled = true isCoreLibraryDesugaringEnabled = true
} }
lint {
// Workaround: NonNullableMutableLiveDataDetector crashes with
// IncompatibleClassChangeError on AGP 8.7.x + Kotlin 2.1.x
disable += "NullSafeMutableLiveData"
}
testOptions { testOptions {
unitTests.isReturnDefaultValues = true unitTests.isReturnDefaultValues = true
} }

View file

@ -6,4 +6,14 @@ plugins {
alias(libs.plugins.kotlin.serialization) apply false alias(libs.plugins.kotlin.serialization) apply false
alias(libs.plugins.ksp) apply false alias(libs.plugins.ksp) apply false
alias(libs.plugins.room) 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)
}
}
} }

113
config/detekt/detekt.yml Normal file
View file

@ -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

View file

@ -2,7 +2,6 @@ package com.librechat.android.core.common.extensions
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.retryWhen import kotlinx.coroutines.flow.retryWhen
import kotlin.math.pow import kotlin.math.pow

View file

@ -12,5 +12,8 @@ fun String.ensureHttps(): String = when {
} }
fun String.truncate(maxLength: Int, ellipsis: String = "..."): String = fun String.truncate(maxLength: Int, ellipsis: String = "..."): String =
if (length <= maxLength) this if (length <= maxLength) {
else take(maxLength - ellipsis.length) + ellipsis this
} else {
take(maxLength - ellipsis.length) + ellipsis
}

View file

@ -15,11 +15,11 @@ class SafeApiCallTest {
@Test @Test
fun `safeApiCall returns Error on exception`() = runTest { fun `safeApiCall returns Error on exception`() = runTest {
val result = safeApiCall<String> { throw RuntimeException("boom") } val result = safeApiCall<String> { throw IllegalStateException("boom") }
assertThat(result).isInstanceOf(Result.Error::class.java) assertThat(result).isInstanceOf(Result.Error::class.java)
val error = result as Result.Error val error = result as Result.Error
assertThat(error.message).isEqualTo("boom") assertThat(error.message).isEqualTo("boom")
assertThat(error.exception).isInstanceOf(RuntimeException::class.java) assertThat(error.exception).isInstanceOf(IllegalStateException::class.java)
} }
@Test @Test

View file

@ -15,14 +15,14 @@ import io.ktor.client.request.header
import io.ktor.client.request.post import io.ktor.client.request.post
import io.ktor.client.request.setBody import io.ktor.client.request.setBody
import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.HttpResponse
import java.io.IOException
import java.security.KeyStoreException
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import timber.log.Timber import timber.log.Timber
import java.io.IOException
import java.security.KeyStoreException
class TokenDataStore( class TokenDataStore(
context: Context, context: Context,

View file

@ -1,9 +1,9 @@
package com.librechat.android.core.data.mapper package com.librechat.android.core.data.mapper
import com.librechat.android.core.data.db.entity.MessageEntity 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.Feedback
import com.librechat.android.core.model.FileReference 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.Message
import com.librechat.android.core.model.MessageContentPart import com.librechat.android.core.model.MessageContentPart
import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.builtins.ListSerializer

View file

@ -27,6 +27,7 @@ interface AgentRepository {
category: String? = null, category: String? = null,
): Result<PaginatedAgents> ): Result<PaginatedAgents>
suspend fun getAgent(id: String): Result<Agent> suspend fun getAgent(id: String): Result<Agent>
/** /**
* Fetches complete agent data for editing via the /expanded endpoint. * Fetches complete agent data for editing via the /expanded endpoint.
* Unlike [getAgent] which returns limited view-only fields, this method * Unlike [getAgent] which returns limited view-only fields, this method

View file

@ -12,12 +12,12 @@ import com.librechat.android.core.model.ToolAuthStatus
import com.librechat.android.core.model.ToolCallRecord import com.librechat.android.core.model.ToolCallRecord
import com.librechat.android.core.model.ToolCallResult import com.librechat.android.core.model.ToolCallResult
import com.librechat.android.core.model.request.CreateActionRequest 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.CreateAgentRequest
import com.librechat.android.core.model.request.RevertAgentRequest import com.librechat.android.core.model.request.RevertAgentRequest
import com.librechat.android.core.model.request.ToolCallRequest import com.librechat.android.core.model.request.ToolCallRequest
import com.librechat.android.core.model.request.UpdateAgentRequest import com.librechat.android.core.model.request.UpdateAgentRequest
import com.librechat.android.core.network.api.AgentsApi import com.librechat.android.core.network.api.AgentsApi
import kotlinx.serialization.json.Json
class AgentRepositoryImpl( class AgentRepositoryImpl(
private val agentsApi: AgentsApi, private val agentsApi: AgentsApi,

View file

@ -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.FileReference
import com.librechat.android.core.model.StreamEvent import com.librechat.android.core.model.StreamEvent
import com.librechat.android.core.model.request.AddedConversation 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.request.EphemeralAgent
import com.librechat.android.core.model.response.ChatStatusResponse import com.librechat.android.core.model.response.ChatStatusResponse
import com.librechat.android.core.network.api.ChatApi import com.librechat.android.core.network.api.ChatApi

View file

@ -1,13 +1,13 @@
package com.librechat.android.core.data.repository package com.librechat.android.core.data.repository
import com.librechat.android.core.common.BackendVersion 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.Result
import com.librechat.android.core.common.result.safeApiCall import com.librechat.android.core.common.result.safeApiCall
import com.librechat.android.core.data.datastore.ConfigCacheDataStore import com.librechat.android.core.data.datastore.ConfigCacheDataStore
import com.librechat.android.core.model.EndpointConfig import com.librechat.android.core.model.EndpointConfig
import com.librechat.android.core.model.StartupConfig import com.librechat.android.core.model.StartupConfig
import com.librechat.android.core.network.api.ConfigApi import com.librechat.android.core.network.api.ConfigApi
import com.librechat.android.core.common.result.ApiException
import io.ktor.client.plugins.HttpRequestTimeoutException import io.ktor.client.plugins.HttpRequestTimeoutException
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow

View file

@ -3,7 +3,6 @@ package com.librechat.android.core.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonObject
@Immutable @Immutable
@Serializable @Serializable

View file

@ -5,64 +5,138 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
enum class EModelEndpoint { enum class EModelEndpoint {
@SerialName("azureOpenAI") AZURE_OPENAI, @SerialName("azureOpenAI")
@SerialName("openAI") OPENAI, AZURE_OPENAI,
@SerialName("google") GOOGLE,
@SerialName("anthropic") ANTHROPIC, @SerialName("openAI")
@SerialName("assistants") ASSISTANTS, OPENAI,
@SerialName("azureAssistants") AZURE_ASSISTANTS,
@SerialName("agents") AGENTS, @SerialName("google")
@SerialName("custom") CUSTOM, GOOGLE,
@SerialName("bedrock") BEDROCK,
@SerialName("anthropic")
ANTHROPIC,
@SerialName("assistants")
ASSISTANTS,
@SerialName("azureAssistants")
AZURE_ASSISTANTS,
@SerialName("agents")
AGENTS,
@SerialName("custom")
CUSTOM,
@SerialName("bedrock")
BEDROCK,
} }
@Serializable @Serializable
enum class ContentType { enum class ContentType {
@SerialName("text") TEXT, @SerialName("text")
@SerialName("think") THINK, TEXT,
@SerialName("text_delta") TEXT_DELTA,
@SerialName("tool_call") TOOL_CALL, @SerialName("think")
@SerialName("image_file") IMAGE_FILE, THINK,
@SerialName("image_url") IMAGE_URL,
@SerialName("video_url") VIDEO_URL, @SerialName("text_delta")
@SerialName("input_audio") INPUT_AUDIO, TEXT_DELTA,
@SerialName("agent_update") AGENT_UPDATE,
@SerialName("error") ERROR, @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 @Serializable
enum class StepType { enum class StepType {
@SerialName("tool_calls") TOOL_CALLS, @SerialName("tool_calls")
@SerialName("message_creation") MESSAGE_CREATION, TOOL_CALLS,
@SerialName("message_creation")
MESSAGE_CREATION,
} }
@Serializable @Serializable
enum class ToolCallType { enum class ToolCallType {
@SerialName("function") FUNCTION, @SerialName("function")
@SerialName("retrieval") RETRIEVAL, FUNCTION,
@SerialName("file_search") FILE_SEARCH,
@SerialName("code_interpreter") CODE_INTERPRETER, @SerialName("retrieval")
@SerialName("tool_call") TOOL_CALL, RETRIEVAL,
@SerialName("file_search")
FILE_SEARCH,
@SerialName("code_interpreter")
CODE_INTERPRETER,
@SerialName("tool_call")
TOOL_CALL,
} }
@Serializable @Serializable
enum class FeedbackRating { enum class FeedbackRating {
@SerialName("thumbsUp") THUMBS_UP, @SerialName("thumbsUp")
@SerialName("thumbsDown") THUMBS_DOWN, THUMBS_UP,
@SerialName("thumbsDown")
THUMBS_DOWN,
} }
@Serializable @Serializable
enum class Provider { enum class Provider {
@SerialName("openAI") OPENAI, @SerialName("openAI")
@SerialName("anthropic") ANTHROPIC, OPENAI,
@SerialName("azureOpenAI") AZURE,
@SerialName("google") GOOGLE, @SerialName("anthropic")
@SerialName("vertexai") VERTEXAI, ANTHROPIC,
@SerialName("bedrock") BEDROCK,
@SerialName("mistralai") MISTRALAI, @SerialName("azureOpenAI")
@SerialName("mistral") MISTRAL, AZURE,
@SerialName("deepseek") DEEPSEEK,
@SerialName("moonshot") MOONSHOT, @SerialName("google")
@SerialName("openrouter") OPENROUTER, GOOGLE,
@SerialName("xai") XAI,
@SerialName("vertexai")
VERTEXAI,
@SerialName("bedrock")
BEDROCK,
@SerialName("mistralai")
MISTRALAI,
@SerialName("mistral")
MISTRAL,
@SerialName("deepseek")
DEEPSEEK,
@SerialName("moonshot")
MOONSHOT,
@SerialName("openrouter")
OPENROUTER,
@SerialName("xai")
XAI,
} }

View file

@ -22,11 +22,24 @@ data class ParameterDefinition(
@Serializable @Serializable
enum class ParameterType { enum class ParameterType {
@SerialName("slider") SLIDER, @SerialName("slider")
@SerialName("dropdown") DROPDOWN, SLIDER,
@SerialName("checkbox") CHECKBOX,
@SerialName("text") TEXT, @SerialName("dropdown")
@SerialName("switch") SWITCH, DROPDOWN,
@SerialName("textarea") TEXTAREA,
@SerialName("tags") TAGS, @SerialName("checkbox")
CHECKBOX,
@SerialName("text")
TEXT,
@SerialName("switch")
SWITCH,
@SerialName("textarea")
TEXTAREA,
@SerialName("tags")
TAGS,
} }

View file

@ -25,11 +25,20 @@ data class McpServer(
@Serializable @Serializable
enum class McpServerType { enum class McpServerType {
@SerialName("sse") SSE, @SerialName("sse")
@SerialName("streamable-http") STREAMABLE_HTTP, SSE,
@SerialName("http") HTTP,
@SerialName("stdio") STDIO, @SerialName("streamable-http")
@SerialName("websocket") WEBSOCKET, STREAMABLE_HTTP,
@SerialName("http")
HTTP,
@SerialName("stdio")
STDIO,
@SerialName("websocket")
WEBSOCKET,
} }
/** Auth mode for the MCP server add/edit dialog. */ /** Auth mode for the MCP server add/edit dialog. */
@ -47,15 +56,23 @@ data class McpApiKeyConfig(
@Serializable @Serializable
enum class McpApiKeySource { enum class McpApiKeySource {
@SerialName("admin") ADMIN, @SerialName("admin")
@SerialName("user") USER, ADMIN,
@SerialName("user")
USER,
} }
@Serializable @Serializable
enum class McpAuthorizationType { enum class McpAuthorizationType {
@SerialName("bearer") BEARER, @SerialName("bearer")
@SerialName("basic") BASIC, BEARER,
@SerialName("custom") CUSTOM,
@SerialName("basic")
BASIC,
@SerialName("custom")
CUSTOM,
} }
/** Maps to backend oauth object. Only the most common fields are exposed in the UI. */ /** Maps to backend oauth object. Only the most common fields are exposed in the UI. */

View file

@ -16,7 +16,6 @@ import com.librechat.android.core.model.request.UpdateAgentRequest
import com.librechat.android.core.model.response.AgentListResponse import com.librechat.android.core.model.response.AgentListResponse
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.call.body import io.ktor.client.call.body
import kotlinx.serialization.json.JsonArray
import io.ktor.client.request.delete import io.ktor.client.request.delete
import io.ktor.client.request.forms.formData import io.ktor.client.request.forms.formData
import io.ktor.client.request.forms.submitFormWithBinaryData 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.Headers
import io.ktor.http.HttpHeaders import io.ktor.http.HttpHeaders
import io.ktor.http.path import io.ktor.http.path
import kotlinx.serialization.json.JsonArray
import timber.log.Timber import timber.log.Timber
class AgentsApi constructor( class AgentsApi constructor(

View file

@ -1,11 +1,11 @@
package com.librechat.android.core.network.api package com.librechat.android.core.network.api
import com.librechat.android.core.model.request.LoginRequest 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.PasswordResetRequest
import com.librechat.android.core.model.request.RegisterRequest import com.librechat.android.core.model.request.RegisterRequest
import com.librechat.android.core.model.request.ResetPasswordRequest import com.librechat.android.core.model.request.ResetPasswordRequest
import com.librechat.android.core.model.request.TwoFactorDisableRequest 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.TwoFactorVerifyRequest
import com.librechat.android.core.model.request.TwoFactorVerifyTempRequest import com.librechat.android.core.model.request.TwoFactorVerifyTempRequest
import com.librechat.android.core.model.response.LoginResponse import com.librechat.android.core.model.response.LoginResponse

View file

@ -1,10 +1,10 @@
package com.librechat.android.core.network.api package com.librechat.android.core.network.api
import com.librechat.android.core.model.Conversation 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.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.ConvoDeleteArg
import com.librechat.android.core.model.request.ConvoDeleteBody
import com.librechat.android.core.model.request.ConvoUpdateArg import com.librechat.android.core.model.request.ConvoUpdateArg
import com.librechat.android.core.model.request.ConvoUpdateBody import com.librechat.android.core.model.request.ConvoUpdateBody
import com.librechat.android.core.model.request.DuplicateConversationRequest import com.librechat.android.core.model.request.DuplicateConversationRequest

View file

@ -54,7 +54,8 @@ class FilesApi constructor(
height: Int? = null, height: Int? = null,
): FileObject { ): FileObject {
Timber.d( 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, filename, type, bytes.size, fileId, endpoint, model, agentId, messageFile, width, height,
) )

View file

@ -35,7 +35,6 @@ object LibreChatHttpClient {
cacheDir: File? = null, cacheDir: File? = null,
debug: Boolean = false, debug: Boolean = false,
): HttpClient = HttpClient(OkHttp) { ): HttpClient = HttpClient(OkHttp) {
engine { engine {
if (cacheDir != null) { if (cacheDir != null) {
config { config {

View file

@ -120,8 +120,7 @@ class SseEventMapper(private val json: Json) {
val parseErrors = mutableListOf<String>() val parseErrors = mutableListOf<String>()
val conversation = root["conversation"]?.let { val conversation = root["conversation"]?.let {
try { json.decodeFromJsonElement(Conversation.serializer(), it) } try { json.decodeFromJsonElement(Conversation.serializer(), it) } catch (e: Exception) {
catch (e: Exception) {
val msg = "Failed to parse final conversation: ${e.message}" val msg = "Failed to parse final conversation: ${e.message}"
Timber.w(e, msg) Timber.w(e, msg)
parseErrors.add(msg) parseErrors.add(msg)
@ -129,8 +128,7 @@ class SseEventMapper(private val json: Json) {
} }
} }
val requestMessage = root["requestMessage"]?.let { val requestMessage = root["requestMessage"]?.let {
try { json.decodeFromJsonElement(Message.serializer(), it) } try { json.decodeFromJsonElement(Message.serializer(), it) } catch (e: Exception) {
catch (e: Exception) {
val msg = "Failed to parse final requestMessage: ${e.message}" val msg = "Failed to parse final requestMessage: ${e.message}"
Timber.w(e, msg) Timber.w(e, msg)
parseErrors.add(msg) parseErrors.add(msg)
@ -138,8 +136,7 @@ class SseEventMapper(private val json: Json) {
} }
} }
val responseMessage = root["responseMessage"]?.let { val responseMessage = root["responseMessage"]?.let {
try { json.decodeFromJsonElement(Message.serializer(), it) } try { json.decodeFromJsonElement(Message.serializer(), it) } catch (e: Exception) {
catch (e: Exception) {
val msg = "Failed to parse final responseMessage: ${e.message}" val msg = "Failed to parse final responseMessage: ${e.message}"
Timber.w(e, msg) Timber.w(e, msg)
parseErrors.add(msg) parseErrors.add(msg)
@ -148,8 +145,7 @@ class SseEventMapper(private val json: Json) {
} }
// Legacy: some events use "message" instead of "responseMessage" // Legacy: some events use "message" instead of "responseMessage"
val legacyMessage = root["message"]?.let { val legacyMessage = root["message"]?.let {
try { json.decodeFromJsonElement(Message.serializer(), it) } try { json.decodeFromJsonElement(Message.serializer(), it) } catch (e: Exception) {
catch (e: Exception) {
val msg = "Failed to parse final legacy message: ${e.message}" val msg = "Failed to parse final legacy message: ${e.message}"
Timber.w(e, msg) Timber.w(e, msg)
parseErrors.add(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 // If ALL critical fields failed to parse, emit an Error instead
val allFieldsNull = conversation == null && requestMessage == null val allFieldsNull = conversation == null && requestMessage == null &&
&& responseMessage == null && legacyMessage == null responseMessage == null && legacyMessage == null
if (allFieldsNull && parseErrors.isNotEmpty()) { if (allFieldsNull && parseErrors.isNotEmpty()) {
Timber.e("Final event: all fields failed to parse -- %s", parseErrors) Timber.e("Final event: all fields failed to parse -- %s", parseErrors)
return StreamEvent.Error( return StreamEvent.Error(
@ -256,7 +252,7 @@ class SseEventMapper(private val json: Json) {
"on_message_delta" -> mapMessageDelta(data, agentId, groupId) "on_message_delta" -> mapMessageDelta(data, agentId, groupId)
"on_reasoning_delta" -> mapReasoningDelta(data, agentId, groupId) "on_reasoning_delta" -> mapReasoningDelta(data, agentId, groupId)
"on_run_step" -> mapRunStep(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_run_step_completed" -> mapRunStepCompleted(data, agentId, groupId)
"on_chat_model_end" -> null "on_chat_model_end" -> null
"on_agent_update" -> 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( private fun mapRunStepCompleted(
data: JsonObject, data: JsonObject,
agentId: String?, agentId: String?,

View file

@ -23,8 +23,8 @@ class SseLineParser(
channel.readUTF8Line() channel.readUTF8Line()
} }
} catch (e: kotlinx.coroutines.TimeoutCancellationException) { } catch (e: kotlinx.coroutines.TimeoutCancellationException) {
Timber.w("SSE stream stalled: no data 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") throw IOException("Stream stalled: no data received for ${lineReadTimeoutMs / 1000}s", e)
} ?: break } ?: break
when { when {

View file

@ -26,13 +26,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.LiveRegionMode import androidx.compose.ui.semantics.LiveRegionMode
import androidx.compose.ui.semantics.liveRegion import androidx.compose.ui.semantics.liveRegion
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.librechat.android.core.model.Banner import com.librechat.android.core.model.Banner
import com.librechat.android.core.ui.R import com.librechat.android.core.ui.R
import androidx.compose.ui.res.stringResource
/** /**
* Renders a vertical stack of dismissible server banners. * Renders a vertical stack of dismissible server banners.

View file

@ -11,6 +11,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview 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.ParameterDefinition
import com.librechat.android.core.model.ParameterType import com.librechat.android.core.model.ParameterType
import com.librechat.android.core.ui.R 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. */ /** Dispatches a list of ParameterDefinitions to typed controls (slider, dropdown, checkbox, input, textarea) by ParameterType. */
@Composable @Composable
@ -113,8 +113,11 @@ fun DynamicParameterPanel(
ParameterType.TAGS -> { ParameterType.TAGS -> {
DynamicTagsInput( DynamicTagsInput(
label = definition.label, label = definition.label,
tags = if (currentValue.isBlank()) emptyList() tags = if (currentValue.isBlank()) {
else currentValue.split("\n").filter { it.isNotBlank() }, emptyList()
} else {
currentValue.split("\n").filter { it.isNotBlank() }
},
onTagsChanged = { tags -> onTagsChanged = { tags ->
onValueChanged(definition.key, tags.joinToString("\n")) onValueChanged(definition.key, tags.joinToString("\n"))
}, },

View file

@ -14,12 +14,12 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import kotlin.math.roundToInt
import com.librechat.android.core.ui.R 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. */ /** Labeled slider that snaps to step increments, with stepCount derived from (max - min) / step. */
@Composable @Composable

View file

@ -24,11 +24,11 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.librechat.android.core.ui.R 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. */ /** Tag input that renders chips and allows adding/removing text tags up to a max count. */
@OptIn(ExperimentalLayoutApi::class) @OptIn(ExperimentalLayoutApi::class)

View file

@ -16,12 +16,12 @@ import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.LiveRegionMode import androidx.compose.ui.semantics.LiveRegionMode
import androidx.compose.ui.semantics.liveRegion import androidx.compose.ui.semantics.liveRegion
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.librechat.android.core.ui.R import com.librechat.android.core.ui.R
import androidx.compose.ui.res.stringResource
@Composable @Composable
fun ErrorBanner( fun ErrorBanner(

View file

@ -12,8 +12,8 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import com.librechat.android.core.ui.R
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import com.librechat.android.core.ui.R
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable

View file

@ -7,10 +7,10 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import com.librechat.android.core.ui.R import com.librechat.android.core.ui.R
import androidx.compose.ui.res.stringResource
@Composable @Composable
fun LoadingIndicator( fun LoadingIndicator(

View file

@ -21,13 +21,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.librechat.android.core.model.ParameterDefinition import com.librechat.android.core.model.ParameterDefinition
import com.librechat.android.core.model.ParameterType import com.librechat.android.core.model.ParameterType
import com.librechat.android.core.ui.R import com.librechat.android.core.ui.R
import androidx.compose.ui.res.stringResource
@Stable @Stable
data class ModelParameters( data class ModelParameters(
@ -174,7 +174,9 @@ fun ModelParameterContent(
onValueChange = { newValue -> onValueChange = { newValue ->
onParametersChanged(parameters.withUpdatedKey(definition.key, 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, description = definition.description,
) )
} }
@ -243,8 +245,11 @@ fun ModelParameterContent(
} }
ParameterType.TAGS -> { ParameterType.TAGS -> {
val tags = if (currentValue.isBlank()) emptyList() val tags = if (currentValue.isBlank()) {
else currentValue.split("\n").filter { it.isNotBlank() } emptyList()
} else {
currentValue.split("\n").filter { it.isNotBlank() }
}
DynamicTagsInput( DynamicTagsInput(
label = definition.label, label = definition.label,

View file

@ -7,10 +7,9 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height 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.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
@ -57,10 +56,10 @@ import androidx.compose.ui.unit.dp
@Composable @Composable
fun OtpVerificationDialog( fun OtpVerificationDialog(
title: String, title: String,
description: String? = null,
isLoading: Boolean = false,
onVerify: (token: String?, backupCode: String?) -> Unit, onVerify: (token: String?, backupCode: String?) -> Unit,
onDismiss: () -> Unit, onDismiss: () -> Unit,
description: String? = null,
isLoading: Boolean = false,
verifyLabel: String = "Verify", verifyLabel: String = "Verify",
cancelLabel: String = "Cancel", cancelLabel: String = "Cancel",
backupCodeLabel: String = "Backup Code", backupCodeLabel: String = "Backup Code",

View file

@ -28,6 +28,7 @@ private const val FALLBACK_CORNER_RADIUS_DP = 24f
@Composable @Composable
fun ScreenTransitionWrapper( fun ScreenTransitionWrapper(
transition: Transition<EnterExitState>, transition: Transition<EnterExitState>,
modifier: Modifier = Modifier,
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
val screenCornerRadiusDp = rememberScreenCornerRadiusDp() val screenCornerRadiusDp = rememberScreenCornerRadiusDp()
@ -43,7 +44,7 @@ fun ScreenTransitionWrapper(
} }
} }
Box( Box(
modifier = Modifier modifier = modifier
.fillMaxSize() .fillMaxSize()
.clip(RoundedCornerShape(cornerRadius.dp)), .clip(RoundedCornerShape(cornerRadius.dp)),
) { ) {

View file

@ -62,8 +62,11 @@ fun LibreChatTheme(
) { ) {
val colorScheme = when { val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
if (darkTheme) dynamicDarkColorScheme(LocalContext.current) if (darkTheme) {
else dynamicLightColorScheme(LocalContext.current) dynamicDarkColorScheme(LocalContext.current)
} else {
dynamicLightColorScheme(LocalContext.current)
}
} }
darkTheme -> libreChatDarkColorScheme darkTheme -> libreChatDarkColorScheme
else -> libreChatLightColorScheme else -> libreChatLightColorScheme

View file

@ -45,6 +45,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.PasswordVisualTransformation 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.ActionMetadata
import com.librechat.android.core.model.request.FunctionTool import com.librechat.android.core.model.request.FunctionTool
import com.librechat.android.feature.agents.AgentActionDisplayData 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.OpenApiSpecParser
import com.librechat.android.feature.agents.util.ParsedFunctionInfo import com.librechat.android.feature.agents.util.ParsedFunctionInfo
import kotlinx.coroutines.delay 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 VALIDATION_DEBOUNCE_MS = 800L
private const val HIDDEN_PLACEHOLDER = "<HIDDEN>" private const val HIDDEN_PLACEHOLDER = "<HIDDEN>"
@ -313,17 +313,29 @@ private fun ActionEditorDialog(
domain = parsedDomain, domain = parsedDomain,
auth = auth, auth = auth,
rawSpec = rawSpec, rawSpec = rawSpec,
apiKey = if (authType == "service_http" && apiKey.isNotBlank() && apiKey != HIDDEN_PLACEHOLDER) { apiKey = if (
authType == "service_http" &&
apiKey.isNotBlank() &&
apiKey != HIDDEN_PLACEHOLDER
) {
apiKey apiKey
} else { } else {
null null
}, },
oauthClientId = if (authType == "oauth" && oauthClientId.isNotBlank() && oauthClientId != HIDDEN_PLACEHOLDER) { oauthClientId = if (
authType == "oauth" &&
oauthClientId.isNotBlank() &&
oauthClientId != HIDDEN_PLACEHOLDER
) {
oauthClientId oauthClientId
} else { } else {
null null
}, },
oauthClientSecret = if (authType == "oauth" && oauthClientSecret.isNotBlank() && oauthClientSecret != HIDDEN_PLACEHOLDER) { oauthClientSecret = if (
authType == "oauth" &&
oauthClientSecret.isNotBlank() &&
oauthClientSecret != HIDDEN_PLACEHOLDER
) {
oauthClientSecret oauthClientSecret
} else { } else {
null null
@ -373,7 +385,8 @@ private fun ActionEditorDialog(
label = { Text(stringResource(R.string.label_openapi_spec)) }, label = { Text(stringResource(R.string.label_openapi_spec)) },
placeholder = { placeholder = {
Text( 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, fontFamily = FontFamily.Monospace,
) )
}, },

View file

@ -26,11 +26,11 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlin.math.roundToInt
import com.librechat.android.feature.agents.R import com.librechat.android.feature.agents.R
import androidx.compose.ui.res.stringResource import kotlin.math.roundToInt
data class AgentAdvancedSettings( data class AgentAdvancedSettings(
val temperature: Float? = null, val temperature: Float? = null,

View file

@ -18,12 +18,12 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.librechat.android.feature.agents.R import com.librechat.android.feature.agents.R
import androidx.compose.ui.res.stringResource
@Composable @Composable
fun AgentAvatarPicker( fun AgentAvatarPicker(

View file

@ -15,10 +15,10 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.librechat.android.feature.agents.R import com.librechat.android.feature.agents.R
import androidx.compose.ui.res.stringResource
data class AgentCapabilities( data class AgentCapabilities(
val artifacts: Boolean = false, val artifacts: Boolean = false,

View file

@ -14,9 +14,9 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.librechat.android.core.model.AgentCategory import com.librechat.android.core.model.AgentCategory
import com.librechat.android.feature.agents.R import com.librechat.android.feature.agents.R
import androidx.compose.ui.res.stringResource
/** /**
* Maps category values to human-readable English labels. * Maps category values to human-readable English labels.

View file

@ -13,9 +13,9 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.librechat.android.feature.agents.R import com.librechat.android.feature.agents.R
import androidx.compose.ui.res.stringResource
@Composable @Composable
fun AgentCodeInterpreterSection( fun AgentCodeInterpreterSection(

View file

@ -13,9 +13,9 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.librechat.android.feature.agents.R import com.librechat.android.feature.agents.R
import androidx.compose.ui.res.stringResource
@Composable @Composable
fun AgentFileSearchSection( fun AgentFileSearchSection(

View file

@ -35,10 +35,10 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.librechat.android.feature.agents.AgentHandoffDisplayData import com.librechat.android.feature.agents.AgentHandoffDisplayData
import com.librechat.android.feature.agents.R import com.librechat.android.feature.agents.R
import androidx.compose.ui.res.stringResource
@Composable @Composable
fun AgentHandoffConfig( fun AgentHandoffConfig(

View file

@ -5,9 +5,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ExpandLess import androidx.compose.material.icons.filled.ExpandLess
@ -23,10 +21,10 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.librechat.android.core.model.mcp.McpTool import com.librechat.android.core.model.mcp.McpTool
import com.librechat.android.feature.agents.R import com.librechat.android.feature.agents.R
import androidx.compose.ui.res.stringResource
/** Hierarchical MCP tool picker grouped by server with checkbox selection. */ /** Hierarchical MCP tool picker grouped by server with checkbox selection. */
@Composable @Composable
@ -57,7 +55,7 @@ fun AgentMcpToolsSelector(
) )
} }
val unknownServerLabel = stringResource(R.string.unknown_server) val unknownServerLabel = stringResource(R.string.unknown_server)
AnimatedVisibility(visible = expanded) { AnimatedVisibility(visible = expanded) {
Column( Column(

View file

@ -16,9 +16,9 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.librechat.android.feature.agents.R import com.librechat.android.feature.agents.R
import androidx.compose.ui.res.stringResource
data class ModelOption( data class ModelOption(
val id: String, val id: String,

View file

@ -29,9 +29,9 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.librechat.android.feature.agents.R import com.librechat.android.feature.agents.R
import androidx.compose.ui.res.stringResource
enum class AgentVisibility(val label: String) { enum class AgentVisibility(val label: String) {
PRIVATE("Private"), PRIVATE("Private"),

View file

@ -9,9 +9,9 @@ import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.librechat.android.feature.agents.R import com.librechat.android.feature.agents.R
import androidx.compose.ui.res.stringResource
@androidx.compose.runtime.Immutable @androidx.compose.runtime.Immutable
data class SupportContactState( data class SupportContactState(

View file

@ -21,9 +21,9 @@ import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.librechat.android.feature.agents.R import com.librechat.android.feature.agents.R
import androidx.compose.ui.res.stringResource
data class AgentVersion( data class AgentVersion(
val version: Int, val version: Int,

View file

@ -42,6 +42,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
@ -49,7 +50,6 @@ import androidx.compose.ui.window.DialogProperties
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.librechat.android.feature.agents.AgentToolDisplayData import com.librechat.android.feature.agents.AgentToolDisplayData
import com.librechat.android.feature.agents.R 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. * Full-screen dialog for selecting agent tools, matching the web app's ToolSelectDialog.

View file

@ -42,20 +42,19 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import org.koin.compose.viewmodel.koinViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle 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.AvatarImage
import com.librechat.android.core.ui.components.ErrorBanner import com.librechat.android.core.ui.components.ErrorBanner
import com.librechat.android.core.ui.components.LoadingIndicator 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.AgentDetailEvent
import com.librechat.android.feature.agents.viewmodel.AgentDetailViewModel import com.librechat.android.feature.agents.viewmodel.AgentDetailViewModel
import com.librechat.android.feature.agents.R import org.koin.compose.viewmodel.koinViewModel
import androidx.compose.ui.res.stringResource import com.librechat.android.core.ui.R as CoreUiR
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@Composable @Composable
@ -228,7 +227,11 @@ fun AgentDetailScreen(
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
Text( 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, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
@ -354,9 +357,11 @@ private fun AgentDetailSection(
Text( Text(
text = content, text = content,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
color = if (dimContent) MaterialTheme.colorScheme.onSurfaceVariant color = if (dimContent) {
else MaterialTheme.colorScheme.onSurface, MaterialTheme.colorScheme.onSurfaceVariant
} else {
MaterialTheme.colorScheme.onSurface
},
) )
} }
} }

View file

@ -48,11 +48,12 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import org.koin.compose.viewmodel.koinViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.librechat.android.core.ui.components.ErrorBanner import com.librechat.android.core.ui.components.ErrorBanner
import com.librechat.android.core.ui.components.LoadingIndicator 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.AgentActionsPanel
import com.librechat.android.feature.agents.components.AgentAdvancedPanel import com.librechat.android.feature.agents.components.AgentAdvancedPanel
import com.librechat.android.feature.agents.components.AgentAvatarPicker 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.components.ToolSelectDialog
import com.librechat.android.feature.agents.viewmodel.AgentEditorEvent import com.librechat.android.feature.agents.viewmodel.AgentEditorEvent
import com.librechat.android.feature.agents.viewmodel.AgentEditorViewModel import com.librechat.android.feature.agents.viewmodel.AgentEditorViewModel
import com.librechat.android.feature.agents.R import org.koin.compose.viewmodel.koinViewModel
import androidx.compose.ui.res.stringResource
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable

View file

@ -41,20 +41,20 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import org.koin.compose.viewmodel.koinViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle 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.AvatarImage
import com.librechat.android.core.ui.components.EmptyState import com.librechat.android.core.ui.components.EmptyState
import com.librechat.android.core.ui.components.ErrorBanner import com.librechat.android.core.ui.components.ErrorBanner
import com.librechat.android.core.ui.components.LibreChatTopBar import com.librechat.android.core.ui.components.LibreChatTopBar
import com.librechat.android.core.ui.components.LoadingIndicator import com.librechat.android.core.ui.components.LoadingIndicator
import com.librechat.android.feature.agents.AgentCardDisplayData import com.librechat.android.feature.agents.AgentCardDisplayData
import com.librechat.android.feature.agents.viewmodel.AgentMarketplaceViewModel
import com.librechat.android.feature.agents.R 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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable

View file

@ -366,6 +366,7 @@ object OpenApiSpecParser {
"Not valid JSON or YAML.\n" + "Not valid JSON or YAML.\n" +
"JSON error: ${jsonError.message}\n" + "JSON error: ${jsonError.message}\n" +
"YAML error: ${yamlError.message}", "YAML error: ${yamlError.message}",
yamlError,
) )
} }
} }

View file

@ -130,8 +130,11 @@ class AgentDetailViewModel(
private fun Agent.toDetailDisplayData(): AgentDetailDisplayData { private fun Agent.toDetailDisplayData(): AgentDetailDisplayData {
val resolvedUrl = avatarUrl?.let { url -> val resolvedUrl = avatarUrl?.let { url ->
if (url.startsWith("http")) url if (url.startsWith("http")) {
else "${serverDataStore.getBaseUrl()}$url" url
} else {
"${serverDataStore.getBaseUrl()}$url"
}
} }
return AgentDetailDisplayData( return AgentDetailDisplayData(
id = id, id = id,

View file

@ -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.AgentRepository
import com.librechat.android.core.data.repository.ConfigRepository import com.librechat.android.core.data.repository.ConfigRepository
import com.librechat.android.core.data.repository.McpRepository 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.ActionMetadata
import com.librechat.android.core.model.Agent
import com.librechat.android.core.model.AgentAction import com.librechat.android.core.model.AgentAction
import com.librechat.android.core.model.AgentCategory import com.librechat.android.core.model.AgentCategory
import com.librechat.android.core.model.AgentTool 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.mcp.McpTool
import com.librechat.android.core.model.request.CreateActionRequest import com.librechat.android.core.model.request.CreateActionRequest
import com.librechat.android.core.model.request.CreateAgentRequest 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.RevertAgentRequest
import com.librechat.android.core.model.request.UpdateAgentRequest 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.AgentActionDisplayData
import com.librechat.android.feature.agents.AgentHandoffDisplayData import com.librechat.android.feature.agents.AgentHandoffDisplayData
import com.librechat.android.feature.agents.AgentToolDisplayData import com.librechat.android.feature.agents.AgentToolDisplayData
import com.librechat.android.feature.agents.components.AgentAdvancedSettings 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.AgentCapabilities
import com.librechat.android.feature.agents.components.AgentSharingState import com.librechat.android.feature.agents.components.AgentSharingState
import com.librechat.android.feature.agents.components.AgentVersion import com.librechat.android.feature.agents.components.AgentVersion
import com.librechat.android.feature.agents.components.AgentVisibility import com.librechat.android.feature.agents.components.AgentVisibility
import com.librechat.android.feature.agents.components.ModelOption import com.librechat.android.feature.agents.components.ModelOption
import com.librechat.android.feature.agents.components.SupportContactState 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.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
@ -887,13 +886,13 @@ class AgentEditorViewModel(
val serverName = matchingTool.serverName val serverName = matchingTool.serverName
if (serverName != null) { if (serverName != null) {
// Store as "toolName_mcp_serverName" format // Store as "toolName_mcp_serverName" format
tools.add("${mcpToolName}${MCP_TOOL_SEPARATOR}${serverName}") tools.add("${mcpToolName}${MCP_TOOL_SEPARATOR}$serverName")
} else { } else {
tools.add(mcpToolName) tools.add(mcpToolName)
} }
} else { } else {
// May be a server name marker // May be a server name marker
tools.add("${MCP_SERVER_MARKER}${MCP_TOOL_SEPARATOR}${mcpToolName}") tools.add("${MCP_SERVER_MARKER}${MCP_TOOL_SEPARATOR}$mcpToolName")
} }
} }

View file

@ -1,8 +1,8 @@
package com.librechat.android.feature.agents.viewmodel package com.librechat.android.feature.agents.viewmodel
import androidx.compose.runtime.Immutable
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.compose.runtime.Immutable
import com.librechat.android.core.common.result.Result import com.librechat.android.core.common.result.Result
import com.librechat.android.core.data.datastore.ServerDataStore import com.librechat.android.core.data.datastore.ServerDataStore
import com.librechat.android.core.data.repository.AgentRepository import com.librechat.android.core.data.repository.AgentRepository
@ -47,8 +47,11 @@ class AgentMarketplaceViewModel(
private fun Agent.toCardDisplayData(): AgentCardDisplayData { private fun Agent.toCardDisplayData(): AgentCardDisplayData {
val resolvedUrl = avatarUrl?.let { url -> val resolvedUrl = avatarUrl?.let { url ->
if (url.startsWith("http")) url if (url.startsWith("http")) {
else "${serverDataStore.getBaseUrl()}$url" url
} else {
"${serverDataStore.getBaseUrl()}$url"
}
} }
return AgentCardDisplayData( return AgentCardDisplayData(
id = id, id = id,

View file

@ -23,14 +23,14 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import org.koin.compose.viewmodel.koinViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.librechat.android.feature.auth.viewmodel.ForgotPasswordViewModel
import com.librechat.android.feature.auth.R 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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable

View file

@ -21,26 +21,26 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import org.koin.compose.viewmodel.koinViewModel
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LifecycleEventEffect import androidx.lifecycle.compose.LifecycleEventEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.librechat.android.feature.auth.viewmodel.LoginViewModel
import com.librechat.android.feature.auth.R 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 @Composable
fun LoginScreen( fun LoginScreen(
onLoginSuccess: () -> Unit, onLoginSuccess: () -> Unit,
onNavigateToRegister: () -> Unit, onNavigateToRegister: () -> Unit,
modifier: Modifier = Modifier,
onNavigateToForgotPassword: () -> Unit = {}, onNavigateToForgotPassword: () -> Unit = {},
onNavigateToTwoFactor: (String) -> Unit = {}, onNavigateToTwoFactor: (String) -> Unit = {},
modifier: Modifier = Modifier,
viewModel: LoginViewModel = koinViewModel(), viewModel: LoginViewModel = koinViewModel(),
) { ) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle() val uiState by viewModel.uiState.collectAsStateWithLifecycle()

View file

@ -20,15 +20,15 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import org.koin.compose.viewmodel.koinViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.librechat.android.feature.auth.viewmodel.RegisterViewModel
import com.librechat.android.feature.auth.R 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 @Composable
fun RegisterScreen( fun RegisterScreen(

View file

@ -23,15 +23,15 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import org.koin.compose.viewmodel.koinViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.librechat.android.feature.auth.viewmodel.ResetPasswordViewModel
import com.librechat.android.feature.auth.R 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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable

View file

@ -19,14 +19,14 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import org.koin.compose.viewmodel.koinViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.librechat.android.feature.auth.viewmodel.ServerUrlViewModel
import com.librechat.android.feature.auth.R 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 @Composable
fun ServerUrlScreen( fun ServerUrlScreen(

View file

@ -26,15 +26,15 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import org.koin.compose.viewmodel.koinViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.librechat.android.feature.auth.viewmodel.TermsViewModel
import com.librechat.android.feature.auth.R 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. */ /** Displays server terms of service loaded from the backend; gates navigation on user acceptance. */
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)

View file

@ -31,16 +31,16 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
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.heading
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import org.koin.compose.viewmodel.koinViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.librechat.android.feature.auth.viewmodel.TwoFactorViewModel
import com.librechat.android.feature.auth.R 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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -81,7 +81,11 @@ fun TwoFactorScreen(
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
) { ) {
Text( 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, style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.semantics { heading() }, modifier = Modifier.semantics { heading() },
) )

View file

@ -24,15 +24,15 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import org.koin.compose.viewmodel.koinViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.librechat.android.feature.auth.viewmodel.VerifyEmailViewModel
import com.librechat.android.feature.auth.R 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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable

View file

@ -1,9 +1,9 @@
package com.librechat.android.feature.auth.viewmodel package com.librechat.android.feature.auth.viewmodel
import androidx.compose.runtime.Immutable
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.librechat.android.core.common.result.Result import com.librechat.android.core.common.result.Result
import androidx.compose.runtime.Immutable
import com.librechat.android.core.data.repository.AuthRepository import com.librechat.android.core.data.repository.AuthRepository
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow

View file

@ -1,10 +1,10 @@
package com.librechat.android.feature.auth.viewmodel package com.librechat.android.feature.auth.viewmodel
import androidx.compose.runtime.Immutable
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.librechat.android.core.common.result.Result import com.librechat.android.core.common.result.Result
import com.librechat.android.core.data.repository.AuthRepository import com.librechat.android.core.data.repository.AuthRepository
import androidx.compose.runtime.Immutable
import com.librechat.android.core.data.repository.ConfigRepository import com.librechat.android.core.data.repository.ConfigRepository
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow

View file

@ -1,10 +1,10 @@
package com.librechat.android.feature.auth.viewmodel package com.librechat.android.feature.auth.viewmodel
import androidx.compose.runtime.Immutable
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.librechat.android.core.common.result.Result import com.librechat.android.core.common.result.Result
import androidx.compose.runtime.Immutable
import com.librechat.android.core.data.repository.AuthRepository import com.librechat.android.core.data.repository.AuthRepository
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow

View file

@ -1,10 +1,10 @@
package com.librechat.android.feature.auth.viewmodel package com.librechat.android.feature.auth.viewmodel
import androidx.compose.runtime.Immutable
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.librechat.android.core.common.result.Result import com.librechat.android.core.common.result.Result
import com.librechat.android.core.data.datastore.ServerDataStore import com.librechat.android.core.data.datastore.ServerDataStore
import androidx.compose.runtime.Immutable
import com.librechat.android.core.data.repository.ConfigRepository import com.librechat.android.core.data.repository.ConfigRepository
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow

View file

@ -1,9 +1,9 @@
package com.librechat.android.feature.auth.viewmodel package com.librechat.android.feature.auth.viewmodel
import androidx.compose.runtime.Immutable
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.librechat.android.core.common.result.Result import com.librechat.android.core.common.result.Result
import androidx.compose.runtime.Immutable
import com.librechat.android.core.data.repository.UserRepository import com.librechat.android.core.data.repository.UserRepository
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow

View file

@ -1,10 +1,10 @@
package com.librechat.android.feature.auth.viewmodel package com.librechat.android.feature.auth.viewmodel
import androidx.compose.runtime.Immutable
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.librechat.android.core.common.result.Result import com.librechat.android.core.common.result.Result
import androidx.compose.runtime.Immutable
import com.librechat.android.core.data.repository.AuthRepository import com.librechat.android.core.data.repository.AuthRepository
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow

View file

@ -1,9 +1,9 @@
package com.librechat.android.feature.auth.viewmodel package com.librechat.android.feature.auth.viewmodel
import androidx.compose.runtime.Immutable
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.librechat.android.core.common.result.Result import com.librechat.android.core.common.result.Result
import androidx.compose.runtime.Immutable
import com.librechat.android.core.data.repository.UserRepository import com.librechat.android.core.data.repository.UserRepository
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay

View file

@ -228,7 +228,7 @@ class RegisterViewModelTest {
viewModel.onNameChanged("Test") viewModel.onNameChanged("Test")
viewModel.onEmailChanged("test@test.com") viewModel.onEmailChanged("test@test.com")
viewModel.onUsernameChanged("testuser") viewModel.onUsernameChanged("testuser")
viewModel.onPasswordChanged("short1234") // 9 chars, less than 12 viewModel.onPasswordChanged("short1234") // 9 chars, less than 12
viewModel.onConfirmPasswordChanged("short1234") viewModel.onConfirmPasswordChanged("short1234")
viewModel.register() viewModel.register()
advanceUntilIdle() advanceUntilIdle()

View file

@ -3,7 +3,7 @@ package com.librechat.android.feature.chat.audio
import android.content.Context import android.content.Context
import android.media.MediaRecorder import android.media.MediaRecorder
import android.os.Build import android.os.Build
import android.util.Log import timber.log.Timber
import java.io.File import java.io.File
/** /**
@ -13,10 +13,6 @@ import java.io.File
*/ */
class VoiceRecorder(private val context: Context) { class VoiceRecorder(private val context: Context) {
companion object {
private const val TAG = "VoiceRecorder"
}
private var mediaRecorder: MediaRecorder? = null private var mediaRecorder: MediaRecorder? = null
private var outputFile: File? = null private var outputFile: File? = null
private var isCurrentlyRecording = false private var isCurrentlyRecording = false
@ -64,7 +60,7 @@ class VoiceRecorder(private val context: Context) {
mediaRecorder = recorder mediaRecorder = recorder
isCurrentlyRecording = true isCurrentlyRecording = true
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to start voice recording", e) Timber.e(e, "Failed to start voice recording")
cleanup() cleanup()
throw e throw e
} }
@ -87,7 +83,7 @@ class VoiceRecorder(private val context: Context) {
outputFile = null outputFile = null
bytes bytes
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to stop voice recording", e) Timber.e(e, "Failed to stop voice recording")
cleanup() cleanup()
null null
} }

View file

@ -21,12 +21,12 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.res.stringResource
import com.librechat.android.feature.chat.R import com.librechat.android.feature.chat.R
/** /**
@ -48,7 +48,8 @@ fun AgentHandoffCard(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val unknownAgent = stringResource(R.string.agent_unknown) 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( Card(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()

View file

@ -19,12 +19,12 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons 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.AttachFile
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Code 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.Extension
import androidx.compose.material.icons.filled.FindInPage
import androidx.compose.material.icons.filled.TravelExplore import androidx.compose.material.icons.filled.TravelExplore
import androidx.compose.material3.AssistChip import androidx.compose.material3.AssistChip
import androidx.compose.material3.AssistChipDefaults import androidx.compose.material3.AssistChipDefaults
@ -43,6 +43,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.role
@ -52,7 +53,6 @@ import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.librechat.android.core.common.ToolConstants import com.librechat.android.core.common.ToolConstants
import com.librechat.android.feature.chat.McpServerDisplayData import com.librechat.android.feature.chat.McpServerDisplayData
import androidx.compose.ui.res.stringResource
import com.librechat.android.feature.chat.R import com.librechat.android.feature.chat.R
/** /**
@ -65,11 +65,9 @@ fun AttachmentChipsRow(
attachedFiles: List<AttachedFile>, attachedFiles: List<AttachedFile>,
enabledTools: Set<String>, enabledTools: Set<String>,
onRemoveFile: (AttachedFile) -> Unit, onRemoveFile: (AttachedFile) -> Unit,
onToggleTool: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
mcpServers: List<McpServerDisplayData> = emptyList(), mcpServers: List<McpServerDisplayData> = emptyList(),
selectedMcpServerNames: Set<String> = emptySet(), selectedMcpServerNames: Set<String> = emptySet(),
onToggleMcpServer: (String) -> Unit = {},
) { ) {
val hasFiles = attachedFiles.isNotEmpty() val hasFiles = attachedFiles.isNotEmpty()
val hasWebSearch = ToolConstants.WEB_SEARCH in enabledTools val hasWebSearch = ToolConstants.WEB_SEARCH in enabledTools

View file

@ -18,7 +18,6 @@ import androidx.compose.ui.viewinterop.AndroidView
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.ByteArrayDataSource import androidx.media3.datasource.ByteArrayDataSource
import androidx.media3.datasource.DataSpec
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.ProgressiveMediaSource import androidx.media3.exoplayer.source.ProgressiveMediaSource
import androidx.media3.ui.PlayerView import androidx.media3.ui.PlayerView
@ -50,7 +49,6 @@ fun AudioContent(
} }
val dataSource = ByteArrayDataSource(audioBytes) val dataSource = ByteArrayDataSource(audioBytes)
val dataSpec = DataSpec(android.net.Uri.parse("data:$mimeType;base64,"))
ExoPlayer.Builder(context).build().apply { ExoPlayer.Builder(context).build().apply {
val mediaSource = ProgressiveMediaSource.Factory { dataSource } val mediaSource = ProgressiveMediaSource.Factory { dataSource }

View file

@ -1,7 +1,6 @@
package com.librechat.android.feature.chat.components package com.librechat.android.feature.chat.components
import android.media.MediaPlayer import android.media.MediaPlayer
import android.util.Log
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@ -33,12 +32,13 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.librechat.android.feature.chat.R
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import timber.log.Timber
import java.io.File import java.io.File
import java.util.Locale 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. */ /** Plays audio via MediaPlayer with play/pause and seekbar. Polls progress every 250ms. Releases on dispose. */
@Composable @Composable
@ -66,7 +66,7 @@ fun AudioContentPlayer(
durationMs = it.duration durationMs = it.duration
} }
setOnErrorListener { _, what, extra -> setOnErrorListener { _, what, extra ->
Log.e("AudioContentPlayer", "MediaPlayer error: what=$what, extra=$extra") Timber.e("MediaPlayer error: what=$what, extra=$extra")
isPlaying = false isPlaying = false
true true
} }
@ -81,7 +81,7 @@ fun AudioContentPlayer(
mediaPlayer.setDataSource(audioUrl) mediaPlayer.setDataSource(audioUrl)
mediaPlayer.prepareAsync() mediaPlayer.prepareAsync()
} catch (e: Exception) { } catch (e: Exception) {
Log.e("AudioContentPlayer", "Failed to set data source", e) Timber.e(e, "Failed to set data source")
} }
} }

View file

@ -33,7 +33,6 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Send import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
@ -59,29 +58,29 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.draw.alpha
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
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.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat import androidx.compose.ui.res.stringResource
import androidx.core.content.FileProvider
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.ImeAction 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.input.TextFieldValue
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import com.librechat.android.feature.chat.McpServerDisplayData import com.librechat.android.feature.chat.McpServerDisplayData
import java.io.File
import com.librechat.android.feature.chat.PromptMentionDisplayData import com.librechat.android.feature.chat.PromptMentionDisplayData
import androidx.compose.ui.res.stringResource
import com.librechat.android.feature.chat.R import com.librechat.android.feature.chat.R
import java.io.File
data class AttachedFile( data class AttachedFile(
val uri: Uri, val uri: Uri,
@ -212,7 +211,6 @@ fun ChatInput(
} }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
val clipboardManager = LocalClipboardManager.current
var showToolsSheet by remember { mutableStateOf(false) } var showToolsSheet by remember { mutableStateOf(false) }
// Use TextFieldValue internally so we can control cursor position. // Use TextFieldValue internally so we can control cursor position.
@ -256,10 +254,8 @@ fun ChatInput(
attachedFiles = attachedFiles, attachedFiles = attachedFiles,
enabledTools = enabledTools, enabledTools = enabledTools,
onRemoveFile = onRemoveFile, onRemoveFile = onRemoveFile,
onToggleTool = onToggleTool,
mcpServers = mcpServers, mcpServers = mcpServers,
selectedMcpServerNames = selectedMcpServerNames, selectedMcpServerNames = selectedMcpServerNames,
onToggleMcpServer = onToggleMcpServer,
) )
Row( Row(
@ -327,7 +323,9 @@ fun ChatInput(
val afterAt = inputText.substring(atIndex + 1) val afterAt = inputText.substring(atIndex + 1)
// Only show suggestions if there's no space after @ (still typing the mention) // Only show suggestions if there's no space after @ (still typing the mention)
if (!afterAt.contains(' ')) afterAt else null if (!afterAt.contains(' ')) afterAt else null
} else null } else {
null
}
} }
} }
@ -339,7 +337,9 @@ fun ChatInput(
group.name.contains(query, ignoreCase = true) || group.name.contains(query, ignoreCase = true) ||
group.command?.contains(query, ignoreCase = true) == true group.command?.contains(query, ignoreCase = true) == true
}.take(5) }.take(5)
} else emptyList() } else {
emptyList()
}
} }
} }
@ -350,7 +350,9 @@ fun ChatInput(
val afterSlash = inputText.substring(1) val afterSlash = inputText.substring(1)
// Only show suggestions if there's no space (still typing the command) // Only show suggestions if there's no space (still typing the command)
if (!afterSlash.contains(' ')) afterSlash else null if (!afterSlash.contains(' ')) afterSlash else null
} else null } else {
null
}
} }
} }
@ -362,7 +364,9 @@ fun ChatInput(
val cmd = group.command val cmd = group.command
cmd != null && cmd.contains(query, ignoreCase = true) cmd != null && cmd.contains(query, ignoreCase = true)
}.take(5) }.take(5)
} else emptyList() } else {
emptyList()
}
} }
} }

View file

@ -30,12 +30,12 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.librechat.android.core.common.ToolConstants import com.librechat.android.core.common.ToolConstants
import com.librechat.android.feature.chat.McpServerDisplayData import com.librechat.android.feature.chat.McpServerDisplayData
import androidx.compose.ui.res.stringResource
import com.librechat.android.feature.chat.R import com.librechat.android.feature.chat.R
data class ToolToggle( data class ToolToggle(

View file

@ -16,10 +16,10 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons 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.AttachFile
import androidx.compose.material.icons.filled.Code import androidx.compose.material.icons.filled.Code
import androidx.compose.material.icons.filled.Extension 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.FindInPage
import androidx.compose.material.icons.filled.PhotoCamera import androidx.compose.material.icons.filled.PhotoCamera
import androidx.compose.material.icons.filled.PhotoLibrary 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.role
@ -54,7 +55,6 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.librechat.android.core.common.ToolConstants import com.librechat.android.core.common.ToolConstants
import com.librechat.android.feature.chat.McpServerDisplayData import com.librechat.android.feature.chat.McpServerDisplayData
import androidx.compose.ui.res.stringResource
import com.librechat.android.feature.chat.R import com.librechat.android.feature.chat.R
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@ -72,6 +72,7 @@ fun ChatToolsBottomSheet(
onOpenModelSelector: () -> Unit, onOpenModelSelector: () -> Unit,
selectedModelDisplay: String?, selectedModelDisplay: String?,
onDismiss: () -> Unit, onDismiss: () -> Unit,
modifier: Modifier = Modifier,
isCodeInterpreterAvailable: Boolean = true, isCodeInterpreterAvailable: Boolean = true,
) { ) {
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
@ -79,6 +80,7 @@ fun ChatToolsBottomSheet(
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
modifier = modifier,
sheetState = sheetState, sheetState = sheetState,
) { ) {
Column( Column(
@ -284,7 +286,6 @@ fun ChatToolsBottomSheet(
} }
} }
} }
} }
} }
} }

View file

@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.text.ClickableText
import androidx.compose.ui.unit.sp
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.OpenInNew import androidx.compose.material.icons.automirrored.filled.OpenInNew
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -24,6 +23,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString 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.unit.sp
import androidx.compose.ui.window.Popup import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties import androidx.compose.ui.window.PopupProperties
import androidx.compose.ui.res.stringResource
import com.librechat.android.feature.chat.R import com.librechat.android.feature.chat.R
/** /**
@ -135,8 +134,8 @@ fun parseCitations(text: String): Pair<AnnotatedString, List<Citation>> {
@Composable @Composable
fun CitationText( fun CitationText(
text: String, text: String,
fontSizeMultiplier: Float = 1.0f,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
fontSizeMultiplier: Float = 1.0f,
) { ) {
val (annotatedString, citations) = remember(text) { parseCitations(text) } val (annotatedString, citations) = remember(text) { parseCitations(text) }
var activeCitation by remember { mutableStateOf<Citation?>(null) } var activeCitation by remember { mutableStateOf<Citation?>(null) }
@ -160,11 +159,14 @@ fun CitationText(
} }
val baseStyle = MaterialTheme.typography.bodyLarge.let { style -> val baseStyle = MaterialTheme.typography.bodyLarge.let { style ->
if (fontSizeMultiplier == 1.0f) style if (fontSizeMultiplier == 1.0f) {
else style.copy( style
fontSize = (style.fontSize.value * fontSizeMultiplier).sp, } else {
lineHeight = (style.lineHeight.value * fontSizeMultiplier).sp, style.copy(
) fontSize = (style.fontSize.value * fontSizeMultiplier).sp,
lineHeight = (style.lineHeight.value * fontSizeMultiplier).sp,
)
}
} }
if (citations.isEmpty()) { if (citations.isEmpty()) {

View file

@ -36,19 +36,19 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext 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.AnnotatedString
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.withStyle 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay
import androidx.compose.ui.res.stringResource
import com.librechat.android.feature.chat.R import com.librechat.android.feature.chat.R
import kotlinx.coroutines.delay
@Composable @Composable
fun CodeBlock( fun CodeBlock(
@ -232,7 +232,6 @@ private val keywordStyle = SpanStyle(color = Color(0xFF569CD6))
private val numberStyle = SpanStyle(color = Color(0xFFB5CEA8)) private val numberStyle = SpanStyle(color = Color(0xFFB5CEA8))
private val typeStyle = SpanStyle(color = Color(0xFF4EC9B0)) private val typeStyle = SpanStyle(color = Color(0xFF4EC9B0))
private val functionStyle = SpanStyle(color = Color(0xFFDCDCAA)) private val functionStyle = SpanStyle(color = Color(0xFFDCDCAA))
private val punctuationStyle = SpanStyle(color = Color(0xFFD4D4D4))
private val annotationStyle = SpanStyle(color = Color(0xFFD7BA7D)) private val annotationStyle = SpanStyle(color = Color(0xFFD7BA7D))
private val constantStyle = SpanStyle(color = Color(0xFF4FC1FF)) private val constantStyle = SpanStyle(color = Color(0xFF4FC1FF))

View file

@ -15,8 +15,8 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons 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.Cancel
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Code import androidx.compose.material.icons.filled.Code
import androidx.compose.material.icons.filled.ExpandLess import androidx.compose.material.icons.filled.ExpandLess
import androidx.compose.material.icons.filled.ExpandMore 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.res.stringResource
import com.librechat.android.feature.chat.R import com.librechat.android.feature.chat.R
/** /**
@ -118,7 +118,14 @@ fun CodeExecutionCard(
val isSuccess = exitCode == 0 val isSuccess = exitCode == 0
Icon( Icon(
imageVector = if (isSuccess) Icons.Default.CheckCircle else Icons.Default.Cancel, 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), modifier = Modifier.size(18.dp),
tint = if (isSuccess) { tint = if (isSuccess) {
MaterialTheme.colorScheme.primary MaterialTheme.colorScheme.primary

View file

@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -17,7 +16,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.librechat.android.feature.chat.R import com.librechat.android.feature.chat.R
@ -36,9 +34,9 @@ fun ComparisonDualPane(
secondaryModelSelector: @Composable () -> Unit, secondaryModelSelector: @Composable () -> Unit,
primaryContent: @Composable () -> Unit, primaryContent: @Composable () -> Unit,
secondaryContent: @Composable () -> Unit, secondaryContent: @Composable () -> Unit,
modifier: Modifier = Modifier,
onContinueWithPrimary: (() -> Unit)? = null, onContinueWithPrimary: (() -> Unit)? = null,
onContinueWithSecondary: (() -> Unit)? = null, onContinueWithSecondary: (() -> Unit)? = null,
modifier: Modifier = Modifier,
) { ) {
Row(modifier = modifier.fillMaxSize()) { Row(modifier = modifier.fillMaxSize()) {
// Primary pane // Primary pane

View file

@ -42,10 +42,10 @@ fun ComparisonTabBar(
secondaryModelName: String, secondaryModelName: String,
primaryContent: @Composable () -> Unit, primaryContent: @Composable () -> Unit,
secondaryContent: @Composable () -> Unit, secondaryContent: @Composable () -> Unit,
modifier: Modifier = Modifier,
onContinueWithPrimary: (() -> Unit)? = null, onContinueWithPrimary: (() -> Unit)? = null,
onContinueWithSecondary: (() -> Unit)? = null, onContinueWithSecondary: (() -> Unit)? = null,
onTabChanged: ((Int) -> Unit)? = null, onTabChanged: ((Int) -> Unit)? = null,
modifier: Modifier = Modifier,
) { ) {
val pagerState = rememberPagerState(pageCount = { 2 }) val pagerState = rememberPagerState(pageCount = { 2 })
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()

View file

@ -39,17 +39,19 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale 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.Role
import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.SubcomposeAsyncImage import coil.compose.SubcomposeAsyncImage
import com.librechat.android.core.common.ToolConstants import com.librechat.android.core.common.ToolConstants
import com.librechat.android.core.model.AgentToolCall import com.librechat.android.core.model.AgentToolCall
import com.librechat.android.core.model.ContentType import com.librechat.android.core.model.ContentType
import com.librechat.android.core.model.MessageContentPart 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.ArtifactButton
import com.librechat.android.feature.chat.components.artifact.ArtifactPanel import com.librechat.android.feature.chat.components.artifact.ArtifactPanel
import com.librechat.android.feature.chat.components.artifact.ArtifactSegment 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.jsonArray
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive 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 } private val lenientJson = Json { ignoreUnknownKeys = true; isLenient = true }
@Composable @Composable
fun ContentPartRenderer( fun ContentPartRenderer(
part: MessageContentPart, part: MessageContentPart,
modifier: Modifier = Modifier,
baseUrl: String = "", baseUrl: String = "",
fontSizeMultiplier: Float = 1.0f, fontSizeMultiplier: Float = 1.0f,
useKatex: Boolean = false, useKatex: Boolean = false,
@ -79,7 +80,6 @@ fun ContentPartRenderer(
searchQuery: String? = null, searchQuery: String? = null,
searchFocusedOccurrence: Int = -1, searchFocusedOccurrence: Int = -1,
onFocusedOccurrencePositioned: ((LayoutCoordinates) -> Unit)? = null, onFocusedOccurrencePositioned: ((LayoutCoordinates) -> Unit)? = null,
modifier: Modifier = Modifier,
) { ) {
val constrainedModifier = modifier.fillMaxWidth() val constrainedModifier = modifier.fillMaxWidth()
when (part.type) { when (part.type) {
@ -282,85 +282,84 @@ fun ContentPartRenderer(
@Composable @Composable
private fun TextContentPart( private fun TextContentPart(
text: String, text: String,
modifier: Modifier = Modifier,
fontSizeMultiplier: Float = 1.0f, fontSizeMultiplier: Float = 1.0f,
useKatex: Boolean = false, useKatex: Boolean = false,
searchQuery: String? = null, searchQuery: String? = null,
searchFocusedOccurrence: Int = -1, searchFocusedOccurrence: Int = -1,
onFocusedOccurrencePositioned: ((LayoutCoordinates) -> Unit)? = null, onFocusedOccurrencePositioned: ((LayoutCoordinates) -> Unit)? = null,
modifier: Modifier = Modifier,
) { ) {
if (text.isBlank()) return if (text.isBlank()) return
val segments = remember(text) { detectArtifacts(text) } val segments = remember(text) { detectArtifacts(text) }
val hasArtifacts = remember(segments) { segments.any { it is ArtifactSegment.ArtifactReference } } 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<com.librechat.android.feature.chat.components.artifact.Artifact?>(null)
}
Column(modifier = modifier) { Column(modifier = modifier) {
segments.forEach { segment -> if (!hasArtifacts) {
when (segment) { MarkdownContent(
is ArtifactSegment.Text -> { text = text,
MarkdownContent( fontSizeMultiplier = fontSizeMultiplier,
text = segment.text, useKatex = useKatex,
fontSizeMultiplier = fontSizeMultiplier, searchQuery = searchQuery,
useKatex = useKatex, searchFocusedOccurrence = searchFocusedOccurrence,
searchQuery = searchQuery, onFocusedOccurrencePositioned = onFocusedOccurrencePositioned,
searchFocusedOccurrence = searchFocusedOccurrence, )
onFocusedOccurrencePositioned = onFocusedOccurrencePositioned, } else {
) val versionMap = remember(segments) { groupArtifactVersions(segments) }
} var activeArtifact by remember {
is ArtifactSegment.ArtifactReference -> { mutableStateOf<com.librechat.android.feature.chat.components.artifact.Artifact?>(null)
val versions = versionMap[segment.artifact.identifier] ?: listOf(segment.artifact) }
Spacer(modifier = Modifier.height(8.dp))
ArtifactButton( segments.forEach { segment ->
artifact = segment.artifact, when (segment) {
onClick = { activeArtifact = segment.artifact }, is ArtifactSegment.Text -> {
versionCount = versions.size, MarkdownContent(
) text = segment.text,
Spacer(modifier = Modifier.height(8.dp)) 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 -> activeArtifact?.let { artifact ->
val versions = versionMap[artifact.identifier] ?: listOf(artifact) val versions = versionMap[artifact.identifier] ?: listOf(artifact)
ArtifactPanel( ArtifactPanel(
artifact = artifact, artifact = artifact,
onDismiss = { activeArtifact = null }, onDismiss = { activeArtifact = null },
versions = versions, versions = versions,
) )
}
}
} }
} }
@Composable @Composable
private fun ThinkingContentPart( private fun ThinkingContentPart(
thinkingText: String, thinkingText: String,
modifier: Modifier = Modifier,
fontSizeMultiplier: Float = 1.0f, fontSizeMultiplier: Float = 1.0f,
useKatex: Boolean = false, useKatex: Boolean = false,
searchQuery: String? = null, searchQuery: String? = null,
searchFocusedOccurrence: Int = -1, searchFocusedOccurrence: Int = -1,
onFocusedOccurrencePositioned: ((LayoutCoordinates) -> Unit)? = null, onFocusedOccurrencePositioned: ((LayoutCoordinates) -> Unit)? = null,
modifier: Modifier = Modifier,
) { ) {
var isExpanded by remember { mutableStateOf(false) } 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( Column(
modifier = modifier modifier = modifier
@ -438,7 +437,8 @@ private fun ToolCallContentPart(
shape = RoundedCornerShape(8.dp), shape = RoundedCornerShape(8.dp),
) { ) {
Column(modifier = Modifier.padding(12.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( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()

View file

@ -16,9 +16,9 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.res.stringResource
import com.librechat.android.feature.chat.R import com.librechat.android.feature.chat.R
/** /**

View file

@ -28,9 +28,9 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.librechat.android.core.model.request.ForkOption import com.librechat.android.core.model.request.ForkOption
import androidx.compose.ui.res.stringResource
import com.librechat.android.feature.chat.R import com.librechat.android.feature.chat.R
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@ -38,12 +38,14 @@ import com.librechat.android.feature.chat.R
fun ForkOptionsBottomSheet( fun ForkOptionsBottomSheet(
onDismiss: () -> Unit, onDismiss: () -> Unit,
onFork: (option: String, splitAtTarget: Boolean) -> Unit, onFork: (option: String, splitAtTarget: Boolean) -> Unit,
modifier: Modifier = Modifier,
) { ) {
val sheetState = rememberModalBottomSheetState() val sheetState = rememberModalBottomSheetState()
var splitAtTarget by remember { mutableStateOf(false) } var splitAtTarget by remember { mutableStateOf(false) }
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
modifier = modifier,
sheetState = sheetState, sheetState = sheetState,
) { ) {
Column( Column(

Some files were not shown because too many files have changed in this diff Show more