feat(chat): add configurable message stats (#1134)
This commit is contained in:
20 files changed
+606
-44
No files matched your search
@@ -24,6 +24,10 @@ data class ServerStoreState(
|
||||
val typingEffectEnabled: Boolean = true,
|
||||
val typingEffectDelayMs: Int = 30,
|
||||
val chatFontScale: Float = 1.0f,
|
||||
val messageStatsEnabled: Boolean = false,
|
||||
val showUserMessageTokens: Boolean = true,
|
||||
val showAssistantMessageTokens: Boolean = true,
|
||||
val showTokensPerSecond: Boolean = true,
|
||||
// App display language. "system" = follow device locale; otherwise a BCP-47
|
||||
// language code such as "en" or "ko". Applied via ContextWrapper in MainActivity.
|
||||
val appLanguage: String = "system",
|
||||
|
||||
@@ -713,6 +713,32 @@ object AuthManager {
|
||||
serverStore.update { it.copy(typingEffectDelayMs = delayMs) }
|
||||
}
|
||||
|
||||
// ── Message stats ───────────────────────────────────────────────────
|
||||
|
||||
fun isMessageStatsEnabled(): Boolean = serverStore.getLatestState().messageStatsEnabled
|
||||
|
||||
fun setMessageStatsEnabled(enabled: Boolean) {
|
||||
serverStore.update { it.copy(messageStatsEnabled = enabled) }
|
||||
}
|
||||
|
||||
fun isUserMessageTokensEnabled(): Boolean = serverStore.getLatestState().showUserMessageTokens
|
||||
|
||||
fun setUserMessageTokensEnabled(enabled: Boolean) {
|
||||
serverStore.update { it.copy(showUserMessageTokens = enabled) }
|
||||
}
|
||||
|
||||
fun isAssistantMessageTokensEnabled(): Boolean = serverStore.getLatestState().showAssistantMessageTokens
|
||||
|
||||
fun setAssistantMessageTokensEnabled(enabled: Boolean) {
|
||||
serverStore.update { it.copy(showAssistantMessageTokens = enabled) }
|
||||
}
|
||||
|
||||
fun isTokensPerSecondEnabled(): Boolean = serverStore.getLatestState().showTokensPerSecond
|
||||
|
||||
fun setTokensPerSecondEnabled(enabled: Boolean) {
|
||||
serverStore.update { it.copy(showTokensPerSecond = enabled) }
|
||||
}
|
||||
|
||||
// ── Chat Font Scale (issue #1004) ───────────────────────────────────
|
||||
|
||||
fun getChatFontScale(): Float = serverStore.getLatestState().chatFontScale
|
||||
|
||||
@@ -95,6 +95,7 @@ import java.time.format.DateTimeFormatter
|
||||
* renderer (issue #866). Agent prose renders full-bleed; user messages keep
|
||||
* this bubble so the conversation stays scannable.
|
||||
*/
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun ChatBubble(
|
||||
message: ChatMessage,
|
||||
@@ -106,6 +107,8 @@ fun ChatBubble(
|
||||
openingAttachmentPath: String? = null,
|
||||
canSaveAttachment: Boolean = true,
|
||||
onImageClick: (ImageViewerModel) -> Unit = {},
|
||||
messageStatsEnabled: Boolean = false,
|
||||
showUserMessageTokens: Boolean = true,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val screenWidth = LocalConfiguration.current.screenWidthDp.dp
|
||||
@@ -222,12 +225,12 @@ fun ChatBubble(
|
||||
}
|
||||
}
|
||||
if (!message.isStreaming) {
|
||||
Row(
|
||||
FlowRow(
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.End)
|
||||
.padding(top = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
itemVerticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
@@ -257,43 +260,28 @@ fun ChatBubble(
|
||||
color = userBubbleTextColor.copy(alpha = 0.6f),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
)
|
||||
if (message.tokenCount != null && message.tokenCount > 0) {
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = "•",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = userBubbleTextColor.copy(alpha = 0.4f),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text =
|
||||
stringResource(
|
||||
R.string.chat_msg_tokens,
|
||||
TokenEstimator.formatTokenCount(message.tokenCount),
|
||||
),
|
||||
color = userBubbleTextColor.copy(alpha = 0.7f),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
modifier = Modifier.testTag("bubble_token_count"),
|
||||
)
|
||||
}
|
||||
if (message.tps != null && message.tps > 0.0) {
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = "•",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = userBubbleTextColor.copy(alpha = 0.4f),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text =
|
||||
stringResource(
|
||||
R.string.chat_msg_tps,
|
||||
TokenEstimator.formatTps(message.tps),
|
||||
),
|
||||
color = userBubbleTextColor.copy(alpha = 0.7f),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
modifier = Modifier.testTag("bubble_tps"),
|
||||
)
|
||||
if (messageStatsEnabled && showUserMessageTokens &&
|
||||
message.tokenCount != null && message.tokenCount > 0
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = "•",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = userBubbleTextColor.copy(alpha = 0.4f),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text =
|
||||
stringResource(
|
||||
R.string.chat_msg_tokens,
|
||||
TokenEstimator.formatTokenCount(message.tokenCount),
|
||||
),
|
||||
color = userBubbleTextColor.copy(alpha = 0.7f),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
modifier = Modifier.testTag("bubble_token_count"),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -671,6 +671,10 @@ fun ChatScreen(
|
||||
searchState = searchState,
|
||||
typingEffectEnabled = state.typingEffectEnabled,
|
||||
typingEffectDelayMs = state.typingEffectDelayMs,
|
||||
messageStatsEnabled = state.messageStatsEnabled,
|
||||
showUserMessageTokens = state.showUserMessageTokens,
|
||||
showAssistantMessageTokens = state.showAssistantMessageTokens,
|
||||
showTokensPerSecond = state.showTokensPerSecond,
|
||||
maxToolCallsPerTurn = state.maxToolCallsPerTurn,
|
||||
isLoading = state.isLoading,
|
||||
isLoadingOlder = state.isLoadingOlder,
|
||||
|
||||
@@ -113,6 +113,10 @@ data class ChatUiState(
|
||||
// Cached settings
|
||||
val typingEffectEnabled: Boolean = true,
|
||||
val typingEffectDelayMs: Int = 30,
|
||||
val messageStatsEnabled: Boolean = false,
|
||||
val showUserMessageTokens: Boolean = true,
|
||||
val showAssistantMessageTokens: Boolean = true,
|
||||
val showTokensPerSecond: Boolean = true,
|
||||
// Commands catalog
|
||||
val commandCatalog: CommandCatalog = CommandCatalog(),
|
||||
// Per-command usage counts for the slash-autocomplete ranking (issue
|
||||
@@ -2530,6 +2534,10 @@ class ChatViewModel(
|
||||
state.copy(
|
||||
typingEffectEnabled = AuthManager.isTypingEffectEnabled(),
|
||||
typingEffectDelayMs = AuthManager.getTypingEffectDelayMs(),
|
||||
messageStatsEnabled = AuthManager.isMessageStatsEnabled(),
|
||||
showUserMessageTokens = AuthManager.isUserMessageTokensEnabled(),
|
||||
showAssistantMessageTokens = AuthManager.isAssistantMessageTokensEnabled(),
|
||||
showTokensPerSecond = AuthManager.isTokensPerSecondEnabled(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package com.m57.hermescontrol.ui.chat.fullbleed
|
||||
|
||||
import android.content.ClipData
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@@ -36,6 +39,7 @@ import com.m57.hermescontrol.ui.chat.ChatMessage
|
||||
import com.m57.hermescontrol.ui.chat.ImageViewerModel
|
||||
import com.m57.hermescontrol.ui.chat.InlineAttachment
|
||||
import com.m57.hermescontrol.ui.chat.MarkdownText
|
||||
import com.m57.hermescontrol.ui.chat.TokenEstimator
|
||||
import com.m57.hermescontrol.ui.chat.components.ReasoningCard
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -48,6 +52,7 @@ import kotlinx.coroutines.launch
|
||||
* trailing copy affordance. User messages keep their bubbles; this composable
|
||||
* is only used for ASSISTANT messages.
|
||||
*/
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
internal fun FullBleedAgentMessage(
|
||||
message: ChatMessage,
|
||||
@@ -60,6 +65,9 @@ internal fun FullBleedAgentMessage(
|
||||
openingAttachmentPath: String? = null,
|
||||
canSaveAttachment: Boolean = true,
|
||||
onImageClick: (ImageViewerModel) -> Unit = {},
|
||||
messageStatsEnabled: Boolean = false,
|
||||
showAssistantMessageTokens: Boolean = true,
|
||||
showTokensPerSecond: Boolean = true,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
@@ -127,8 +135,16 @@ internal fun FullBleedAgentMessage(
|
||||
}
|
||||
|
||||
if (!message.isStreaming && message.content.isNotBlank()) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
val showTokenStat =
|
||||
messageStatsEnabled && showAssistantMessageTokens &&
|
||||
message.tokenCount != null && message.tokenCount > 0
|
||||
val showTpsStat =
|
||||
messageStatsEnabled && showTokensPerSecond &&
|
||||
message.tps != null && message.tps > 0.0
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
itemVerticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
@@ -146,7 +162,40 @@ internal fun FullBleedAgentMessage(
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
if (showTokenStat) {
|
||||
AssistantStatItem(
|
||||
value =
|
||||
stringResource(
|
||||
R.string.chat_msg_tokens,
|
||||
TokenEstimator.formatTokenCount(message.tokenCount),
|
||||
),
|
||||
testTag = "fullbleed_token_count",
|
||||
)
|
||||
}
|
||||
if (showTpsStat) {
|
||||
AssistantStatItem(
|
||||
value =
|
||||
stringResource(
|
||||
R.string.chat_msg_tps,
|
||||
TokenEstimator.formatTps(message.tps),
|
||||
),
|
||||
testTag = "fullbleed_tps",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AssistantStatItem(
|
||||
value: String,
|
||||
testTag: String,
|
||||
) {
|
||||
Text(
|
||||
text = value,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.testTag(testTag),
|
||||
)
|
||||
}
|
||||
@@ -64,6 +64,10 @@ fun FullBleedChatList(
|
||||
searchState: ChatSearchState,
|
||||
typingEffectEnabled: Boolean,
|
||||
typingEffectDelayMs: Int,
|
||||
messageStatsEnabled: Boolean = false,
|
||||
showUserMessageTokens: Boolean = true,
|
||||
showAssistantMessageTokens: Boolean = true,
|
||||
showTokensPerSecond: Boolean = true,
|
||||
maxToolCallsPerTurn: Int? = null,
|
||||
isLoading: Boolean,
|
||||
isLoadingOlder: Boolean,
|
||||
@@ -184,6 +188,8 @@ fun FullBleedChatList(
|
||||
savingAttachmentPath = savingAttachmentPath,
|
||||
openingAttachmentPath = openingAttachmentPath,
|
||||
onImageClick = onImageClick,
|
||||
messageStatsEnabled = messageStatsEnabled,
|
||||
showUserMessageTokens = showUserMessageTokens,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -248,6 +254,9 @@ fun FullBleedChatList(
|
||||
openingAttachmentPath = openingAttachmentPath,
|
||||
canSaveAttachment = savingAttachmentPath == null,
|
||||
onImageClick = onImageClick,
|
||||
messageStatsEnabled = messageStatsEnabled,
|
||||
showAssistantMessageTokens = showAssistantMessageTokens,
|
||||
showTokensPerSecond = showTokensPerSecond,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -356,6 +365,8 @@ private fun renderChatBubble(
|
||||
savingAttachmentPath: String?,
|
||||
openingAttachmentPath: String?,
|
||||
onImageClick: (ImageViewerModel) -> Unit,
|
||||
messageStatsEnabled: Boolean,
|
||||
showUserMessageTokens: Boolean,
|
||||
) {
|
||||
ChatBubble(
|
||||
message = message,
|
||||
@@ -367,5 +378,7 @@ private fun renderChatBubble(
|
||||
openingAttachmentPath = openingAttachmentPath,
|
||||
canSaveAttachment = savingAttachmentPath == null,
|
||||
onImageClick = onImageClick,
|
||||
messageStatsEnabled = messageStatsEnabled,
|
||||
showUserMessageTokens = showUserMessageTokens,
|
||||
)
|
||||
}
|
||||
@@ -199,6 +199,14 @@ internal fun SettingsChatPage(
|
||||
onTypingEffectDelayMsChange = viewModel::onTypingEffectDelayMsChange,
|
||||
chatFontScale = state.chatFontScale,
|
||||
onChatFontScaleChange = viewModel::onChatFontScaleChange,
|
||||
messageStatsEnabled = state.messageStatsEnabled,
|
||||
onMessageStatsEnabledChange = viewModel::onMessageStatsEnabledChange,
|
||||
showUserMessageTokens = state.showUserMessageTokens,
|
||||
onUserMessageTokensChange = viewModel::onUserMessageTokensChange,
|
||||
showAssistantMessageTokens = state.showAssistantMessageTokens,
|
||||
onAssistantMessageTokensChange = viewModel::onAssistantMessageTokensChange,
|
||||
showTokensPerSecond = state.showTokensPerSecond,
|
||||
onTokensPerSecondChange = viewModel::onTokensPerSecondChange,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,10 @@ data class SettingsUiState(
|
||||
val typingEffectEnabled: Boolean = false,
|
||||
val typingEffectDelayMs: Int = 30,
|
||||
val chatFontScale: Float = 1.0f,
|
||||
val messageStatsEnabled: Boolean = false,
|
||||
val showUserMessageTokens: Boolean = true,
|
||||
val showAssistantMessageTokens: Boolean = true,
|
||||
val showTokensPerSecond: Boolean = true,
|
||||
val profiles: List<ConnectionProfile> = emptyList(),
|
||||
val selectedProfileId: String? = null,
|
||||
val renameProfileName: String = "",
|
||||
@@ -80,6 +84,10 @@ class SettingsViewModel(
|
||||
val typingEffectEnabled = AuthManager.isTypingEffectEnabled()
|
||||
val typingEffectDelayMs = AuthManager.getTypingEffectDelayMs()
|
||||
val chatFontScale = AuthManager.getChatFontScale()
|
||||
val messageStatsEnabled = AuthManager.isMessageStatsEnabled()
|
||||
val showUserMessageTokens = AuthManager.isUserMessageTokensEnabled()
|
||||
val showAssistantMessageTokens = AuthManager.isAssistantMessageTokensEnabled()
|
||||
val showTokensPerSecond = AuthManager.isTokensPerSecondEnabled()
|
||||
val profiles = AuthManager.getConnectionProfiles()
|
||||
val appLanguage = AuthManager.getAppLanguage()
|
||||
val renameProfileName =
|
||||
@@ -101,6 +109,10 @@ class SettingsViewModel(
|
||||
typingEffectEnabled = typingEffectEnabled,
|
||||
typingEffectDelayMs = typingEffectDelayMs,
|
||||
chatFontScale = chatFontScale,
|
||||
messageStatsEnabled = messageStatsEnabled,
|
||||
showUserMessageTokens = showUserMessageTokens,
|
||||
showAssistantMessageTokens = showAssistantMessageTokens,
|
||||
showTokensPerSecond = showTokensPerSecond,
|
||||
profiles = profiles,
|
||||
selectedProfileId = selectedId,
|
||||
renameProfileName = renameProfileName,
|
||||
@@ -327,6 +339,26 @@ class SettingsViewModel(
|
||||
AuthManager.setChatFontScale(scale)
|
||||
}
|
||||
|
||||
fun onMessageStatsEnabledChange(enabled: Boolean) {
|
||||
_uiState.update { it.copy(messageStatsEnabled = enabled, isSaved = false) }
|
||||
AuthManager.setMessageStatsEnabled(enabled)
|
||||
}
|
||||
|
||||
fun onUserMessageTokensChange(enabled: Boolean) {
|
||||
_uiState.update { it.copy(showUserMessageTokens = enabled, isSaved = false) }
|
||||
AuthManager.setUserMessageTokensEnabled(enabled)
|
||||
}
|
||||
|
||||
fun onAssistantMessageTokensChange(enabled: Boolean) {
|
||||
_uiState.update { it.copy(showAssistantMessageTokens = enabled, isSaved = false) }
|
||||
AuthManager.setAssistantMessageTokensEnabled(enabled)
|
||||
}
|
||||
|
||||
fun onTokensPerSecondChange(enabled: Boolean) {
|
||||
_uiState.update { it.copy(showTokensPerSecond = enabled, isSaved = false) }
|
||||
AuthManager.setTokensPerSecondEnabled(enabled)
|
||||
}
|
||||
|
||||
/** Clear all auth credentials — logs out and returns to landing screen. */
|
||||
fun logout() {
|
||||
AuthManager.setToken(null)
|
||||
@@ -355,6 +387,10 @@ class SettingsViewModel(
|
||||
AuthManager.setThemePreset(state.themePreset)
|
||||
AuthManager.setTypingEffectEnabled(state.typingEffectEnabled)
|
||||
AuthManager.setTypingEffectDelayMs(state.typingEffectDelayMs)
|
||||
AuthManager.setMessageStatsEnabled(state.messageStatsEnabled)
|
||||
AuthManager.setUserMessageTokensEnabled(state.showUserMessageTokens)
|
||||
AuthManager.setAssistantMessageTokensEnabled(state.showAssistantMessageTokens)
|
||||
AuthManager.setTokensPerSecondEnabled(state.showTokensPerSecond)
|
||||
ApiClient.rebuild()
|
||||
|
||||
viewModelScope.launch(ioDispatcher) {
|
||||
|
||||
@@ -32,6 +32,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.Density
|
||||
@@ -204,6 +205,14 @@ internal fun ChatSection(
|
||||
onTypingEffectDelayMsChange: (Int) -> Unit,
|
||||
chatFontScale: Float = 1.0f,
|
||||
onChatFontScaleChange: (Float) -> Unit = {},
|
||||
messageStatsEnabled: Boolean = false,
|
||||
onMessageStatsEnabledChange: (Boolean) -> Unit = {},
|
||||
showUserMessageTokens: Boolean = true,
|
||||
onUserMessageTokensChange: (Boolean) -> Unit = {},
|
||||
showAssistantMessageTokens: Boolean = true,
|
||||
onAssistantMessageTokensChange: (Boolean) -> Unit = {},
|
||||
showTokensPerSecond: Boolean = true,
|
||||
onTokensPerSecondChange: (Boolean) -> Unit = {},
|
||||
) {
|
||||
val fontScaleOptions = listOf(0.85f, 1.0f, 1.15f, 1.30f, 1.50f)
|
||||
val currentIndex =
|
||||
@@ -369,6 +378,53 @@ internal fun ChatSection(
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.settings_item_message_stats),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.settings_desc_message_stats),
|
||||
modifier = Modifier.weight(1f),
|
||||
style =
|
||||
MaterialTheme.typography.bodySmall.copy(
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
)
|
||||
Switch(
|
||||
checked = messageStatsEnabled,
|
||||
onCheckedChange = onMessageStatsEnabledChange,
|
||||
modifier = Modifier.testTag("settings_message_stats"),
|
||||
)
|
||||
}
|
||||
MessageStatsOption(
|
||||
label = stringResource(R.string.settings_item_user_message_tokens),
|
||||
checked = showUserMessageTokens,
|
||||
enabled = messageStatsEnabled,
|
||||
onCheckedChange = onUserMessageTokensChange,
|
||||
testTag = "settings_user_message_tokens",
|
||||
)
|
||||
MessageStatsOption(
|
||||
label = stringResource(R.string.settings_item_assistant_message_tokens),
|
||||
checked = showAssistantMessageTokens,
|
||||
enabled = messageStatsEnabled,
|
||||
onCheckedChange = onAssistantMessageTokensChange,
|
||||
testTag = "settings_assistant_message_tokens",
|
||||
)
|
||||
MessageStatsOption(
|
||||
label = stringResource(R.string.settings_item_tokens_per_second),
|
||||
checked = showTokensPerSecond,
|
||||
enabled = messageStatsEnabled,
|
||||
onCheckedChange = onTokensPerSecondChange,
|
||||
testTag = "settings_tps",
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
@@ -440,3 +496,37 @@ internal fun ChatSection(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MessageStatsOption(
|
||||
label: String,
|
||||
checked: Boolean,
|
||||
enabled: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
testTag: String,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(start = 12.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
style =
|
||||
MaterialTheme.typography.bodyMedium.copy(
|
||||
color =
|
||||
if (enabled) {
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
|
||||
},
|
||||
),
|
||||
)
|
||||
Switch(
|
||||
checked = checked,
|
||||
enabled = enabled,
|
||||
onCheckedChange = onCheckedChange,
|
||||
modifier = Modifier.testTag(testTag),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user