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

View file

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

View file

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

View file

@ -3,22 +3,16 @@ package com.librechat.android.navigation
import androidx.compose.runtime.Immutable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.librechat.android.core.common.extensions.toInstantOrNull
import com.librechat.android.core.common.extensions.toRelativeDateGroup
import com.librechat.android.core.common.result.Result
import com.librechat.android.core.data.repository.AuthRepository
import com.librechat.android.core.data.repository.BannerRepository
import com.librechat.android.core.data.repository.ConfigRepository
import com.librechat.android.core.data.repository.ConversationRepository
import com.librechat.android.core.data.repository.VersionCheckResult
import com.librechat.android.core.common.result.Result
import com.librechat.android.core.common.extensions.toInstantOrNull
import com.librechat.android.core.common.extensions.toRelativeDateGroup
import com.librechat.android.core.model.Banner
import com.librechat.android.core.model.Conversation
import com.librechat.android.core.model.EModelEndpoint
import com.librechat.android.core.network.client.TokenManager
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.temporal.ChronoUnit
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
@ -32,6 +26,10 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import timber.log.Timber
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.temporal.ChronoUnit
/**
* Combined UI state for the drawer sidebar. A single emission replaces
@ -155,7 +153,13 @@ class NavHostViewModel(
* not the entire PhoneLayout/TabletLayout.
*/
val drawerUiState: StateFlow<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)
},
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.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import com.librechat.android.R
import androidx.compose.ui.res.stringResource
/**
* Settings sidebar content shown when the sidebar mode is [SidebarMode.Settings].

View file

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

View file

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

View file

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

View file

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

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

View file

@ -6,4 +6,14 @@ plugins {
alias(libs.plugins.kotlin.serialization) apply false
alias(libs.plugins.ksp) apply false
alias(libs.plugins.room) apply false
alias(libs.plugins.detekt) apply false
alias(libs.plugins.kover)
}
dependencies {
subprojects.forEach { subproject ->
if (subproject.buildFile.exists()) {
kover(subproject)
}
}
}

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.flow.Flow
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.retryWhen
import kotlin.math.pow

View file

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

View file

@ -15,11 +15,11 @@ class SafeApiCallTest {
@Test
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)
val error = result as Result.Error
assertThat(error.message).isEqualTo("boom")
assertThat(error.exception).isInstanceOf(RuntimeException::class.java)
assertThat(error.exception).isInstanceOf(IllegalStateException::class.java)
}
@Test

View file

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

View file

@ -1,9 +1,9 @@
package com.librechat.android.core.data.mapper
import com.librechat.android.core.data.db.entity.MessageEntity
import com.librechat.android.core.model.Attachment
import com.librechat.android.core.model.Feedback
import com.librechat.android.core.model.FileReference
import com.librechat.android.core.model.Attachment
import com.librechat.android.core.model.Message
import com.librechat.android.core.model.MessageContentPart
import kotlinx.serialization.builtins.ListSerializer

View file

@ -27,6 +27,7 @@ interface AgentRepository {
category: String? = null,
): Result<PaginatedAgents>
suspend fun getAgent(id: String): Result<Agent>
/**
* Fetches complete agent data for editing via the /expanded endpoint.
* 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.ToolCallResult
import com.librechat.android.core.model.request.CreateActionRequest
import kotlinx.serialization.json.Json
import com.librechat.android.core.model.request.CreateAgentRequest
import com.librechat.android.core.model.request.RevertAgentRequest
import com.librechat.android.core.model.request.ToolCallRequest
import com.librechat.android.core.model.request.UpdateAgentRequest
import com.librechat.android.core.network.api.AgentsApi
import kotlinx.serialization.json.Json
class AgentRepositoryImpl(
private val agentsApi: AgentsApi,

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,11 +1,11 @@
package com.librechat.android.core.network.api
import com.librechat.android.core.model.request.LoginRequest
import com.librechat.android.core.model.request.OtpVerificationRequest
import com.librechat.android.core.model.request.PasswordResetRequest
import com.librechat.android.core.model.request.RegisterRequest
import com.librechat.android.core.model.request.ResetPasswordRequest
import com.librechat.android.core.model.request.TwoFactorDisableRequest
import com.librechat.android.core.model.request.OtpVerificationRequest
import com.librechat.android.core.model.request.TwoFactorVerifyRequest
import com.librechat.android.core.model.request.TwoFactorVerifyTempRequest
import com.librechat.android.core.model.response.LoginResponse

View file

@ -1,10 +1,10 @@
package com.librechat.android.core.network.api
import com.librechat.android.core.model.Conversation
import com.librechat.android.core.model.request.ArchiveConversationRequest
import com.librechat.android.core.model.request.ArchiveConversationArg
import com.librechat.android.core.model.request.ConvoDeleteBody
import com.librechat.android.core.model.request.ArchiveConversationRequest
import com.librechat.android.core.model.request.ConvoDeleteArg
import com.librechat.android.core.model.request.ConvoDeleteBody
import com.librechat.android.core.model.request.ConvoUpdateArg
import com.librechat.android.core.model.request.ConvoUpdateBody
import com.librechat.android.core.model.request.DuplicateConversationRequest

View file

@ -54,7 +54,8 @@ class FilesApi constructor(
height: Int? = null,
): FileObject {
Timber.d(
"uploadFile: filename=%s, type=%s, size=%d bytes, fileId=%s, endpoint=%s, model=%s, agentId=%s, messageFile=%s, width=%s, height=%s",
"uploadFile: filename=%s, type=%s, size=%d bytes, fileId=%s, " +
"endpoint=%s, model=%s, agentId=%s, messageFile=%s, width=%s, height=%s",
filename, type, bytes.size, fileId, endpoint, model, agentId, messageFile, width, height,
)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -31,16 +31,16 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import org.koin.compose.viewmodel.koinViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.librechat.android.feature.auth.viewmodel.TwoFactorViewModel
import com.librechat.android.feature.auth.R
import androidx.compose.ui.res.stringResource
import com.librechat.android.feature.auth.viewmodel.TwoFactorViewModel
import org.koin.compose.viewmodel.koinViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -81,7 +81,11 @@ fun TwoFactorScreen(
verticalArrangement = Arrangement.Center,
) {
Text(
text = if (uiState.isBackupMode) stringResource(R.string.enter_backup_code) else stringResource(R.string.enter_verification_code),
text = if (uiState.isBackupMode) {
stringResource(R.string.enter_backup_code)
} else {
stringResource(R.string.enter_verification_code)
},
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.semantics { heading() },
)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -19,12 +19,12 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.InsertDriveFile
import androidx.compose.material.icons.filled.AttachFile
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Code
import androidx.compose.material.icons.filled.FindInPage
import androidx.compose.material.icons.automirrored.filled.InsertDriveFile
import androidx.compose.material.icons.filled.Extension
import androidx.compose.material.icons.filled.FindInPage
import androidx.compose.material.icons.filled.TravelExplore
import androidx.compose.material3.AssistChip
import androidx.compose.material3.AssistChipDefaults
@ -43,6 +43,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.role
@ -52,7 +53,6 @@ import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.librechat.android.core.common.ToolConstants
import com.librechat.android.feature.chat.McpServerDisplayData
import androidx.compose.ui.res.stringResource
import com.librechat.android.feature.chat.R
/**
@ -65,11 +65,9 @@ fun AttachmentChipsRow(
attachedFiles: List<AttachedFile>,
enabledTools: Set<String>,
onRemoveFile: (AttachedFile) -> Unit,
onToggleTool: (String) -> Unit,
modifier: Modifier = Modifier,
mcpServers: List<McpServerDisplayData> = emptyList(),
selectedMcpServerNames: Set<String> = emptySet(),
onToggleMcpServer: (String) -> Unit = {},
) {
val hasFiles = attachedFiles.isNotEmpty()
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.util.UnstableApi
import androidx.media3.datasource.ByteArrayDataSource
import androidx.media3.datasource.DataSpec
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.ProgressiveMediaSource
import androidx.media3.ui.PlayerView
@ -50,7 +49,6 @@ fun AudioContent(
}
val dataSource = ByteArrayDataSource(audioBytes)
val dataSpec = DataSpec(android.net.Uri.parse("data:$mimeType;base64,"))
ExoPlayer.Builder(context).build().apply {
val mediaSource = ProgressiveMediaSource.Factory { dataSource }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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