From fb80be04b48e6d555f25d9657392998fb6aecfcb Mon Sep 17 00:00:00 2001 From: M57 <41645758+Hy4ri@users.noreply.github.com> Date: Thu, 3 Sep 2026 15:41:22 +0300 Subject: [PATCH] feat(bots): increase group chat limits to 50/20 with warning banner and confirmation dialog (#1049) --- .../ui/bots/group/GroupChatScreen.kt | 142 ++++++++++++++++-- .../ui/bots/group/GroupChatViewModel.kt | 8 +- app/src/main/res/values-ar/strings.xml | 4 + app/src/main/res/values-ko/strings.xml | 4 + app/src/main/res/values-zh/strings.xml | 4 + app/src/main/res/values/strings.xml | 4 + .../ui/bots/group/GroupChatViewModelTest.kt | 27 ++++ 7 files changed, 180 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/m57/hermescontrol/ui/bots/group/GroupChatScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/bots/group/GroupChatScreen.kt index 9900a1a0..424b9160 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/bots/group/GroupChatScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/bots/group/GroupChatScreen.kt @@ -42,8 +42,10 @@ import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Stop import androidx.compose.material.icons.filled.Terminal +import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -549,6 +551,11 @@ private fun GroupChatSettingsDialog( var maxMessages by remember(currentMaxMessages) { mutableStateOf(currentMaxMessages.toFloat()) } var maxPasses by remember(currentMaxPasses) { mutableStateOf(currentMaxPasses.toFloat()) } var systemPrompt by remember(currentSystemPrompt) { mutableStateOf(currentSystemPrompt.orEmpty()) } + var showHighLimitsConfirmation by remember { mutableStateOf(false) } + + val isHighLimit = + maxMessages.roundToInt() > WARN_MAX_BOT_MESSAGES || + maxPasses.roundToInt() > WARN_MAX_CONTINUATION_PASSES AlertDialog( onDismissRequest = onDismiss, @@ -579,7 +586,12 @@ private fun GroupChatSettingsDialog( Text( text = "${maxMessages.roundToInt()}", style = MaterialTheme.typography.labelLarge, - color = MaterialTheme.colorScheme.primary, + color = + if (maxMessages.roundToInt() > WARN_MAX_BOT_MESSAGES) { + MaterialTheme.colorScheme.error + } else { + MaterialTheme.colorScheme.primary + }, fontWeight = FontWeight.Bold, ) } @@ -595,8 +607,8 @@ private fun GroupChatSettingsDialog( Slider( value = maxMessages, onValueChange = { maxMessages = it }, - valueRange = 1f..20f, - steps = 18, + valueRange = 1f..MAX_ALLOWED_BOT_MESSAGES.toFloat(), + steps = MAX_ALLOWED_BOT_MESSAGES - 2, modifier = Modifier.testTag("max_messages_slider"), ) } @@ -616,7 +628,12 @@ private fun GroupChatSettingsDialog( Text( text = "${maxPasses.roundToInt()}", style = MaterialTheme.typography.labelLarge, - color = MaterialTheme.colorScheme.primary, + color = + if (maxPasses.roundToInt() > WARN_MAX_CONTINUATION_PASSES) { + MaterialTheme.colorScheme.error + } else { + MaterialTheme.colorScheme.primary + }, fontWeight = FontWeight.Bold, ) } @@ -632,12 +649,52 @@ private fun GroupChatSettingsDialog( Slider( value = maxPasses, onValueChange = { maxPasses = it }, - valueRange = 0f..6f, - steps = 5, + valueRange = 0f..MAX_ALLOWED_CONTINUATION_PASSES.toFloat(), + steps = MAX_ALLOWED_CONTINUATION_PASSES - 1, modifier = Modifier.testTag("max_handoffs_slider"), ) } + // Live In-Dialog Warning Banner (Option C) + AnimatedVisibility( + visible = isHighLimit, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically(), + ) { + Surface( + color = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.35f), + contentColor = MaterialTheme.colorScheme.onErrorContainer, + shape = RoundedCornerShape(12.dp), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.error.copy(alpha = 0.5f)), + modifier = + Modifier + .fillMaxWidth() + .testTag("high_limits_warning_banner"), + ) { + Row( + modifier = Modifier.padding(12.dp), + verticalAlignment = Alignment.Top, + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + Icon( + imageVector = Icons.Filled.Warning, + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.size(20.dp), + ) + Text( + text = + stringResource( + R.string.group_chat_settings_high_limits_warning_desc, + maxMessages.roundToInt(), + maxPasses.roundToInt(), + ), + style = MaterialTheme.typography.bodySmall, + ) + } + } + } + // Room Instructions / System Prompt Column { Text( @@ -674,11 +731,15 @@ private fun GroupChatSettingsDialog( confirmButton = { Button( onClick = { - onSave( - maxMessages.roundToInt(), - maxPasses.roundToInt(), - systemPrompt.trim().ifBlank { null }, - ) + if (isHighLimit) { + showHighLimitsConfirmation = true + } else { + onSave( + maxMessages.roundToInt(), + maxPasses.roundToInt(), + systemPrompt.trim().ifBlank { null }, + ) + } }, modifier = Modifier.testTag("save_settings_button"), ) { @@ -702,6 +763,65 @@ private fun GroupChatSettingsDialog( } }, ) + + if (showHighLimitsConfirmation) { + AlertDialog( + onDismissRequest = { showHighLimitsConfirmation = false }, + icon = { + Icon( + imageVector = Icons.Filled.Warning, + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + ) + }, + title = { + Text( + text = stringResource(R.string.group_chat_settings_high_limits_warning_title), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + ) + }, + text = { + Text( + text = + stringResource( + R.string.group_chat_settings_high_limits_warning_desc, + maxMessages.roundToInt(), + maxPasses.roundToInt(), + ), + style = MaterialTheme.typography.bodyMedium, + ) + }, + confirmButton = { + Button( + onClick = { + showHighLimitsConfirmation = false + onSave( + maxMessages.roundToInt(), + maxPasses.roundToInt(), + systemPrompt.trim().ifBlank { null }, + ) + }, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.error, + contentColor = MaterialTheme.colorScheme.onError, + ), + modifier = Modifier.testTag("confirm_high_limits_button"), + ) { + Text(stringResource(R.string.group_chat_settings_high_limits_confirm)) + } + }, + dismissButton = { + TextButton( + onClick = { showHighLimitsConfirmation = false }, + modifier = Modifier.testTag("dismiss_high_limits_button"), + ) { + Text(stringResource(R.string.group_chat_settings_high_limits_adjust)) + } + }, + ) + } } @Composable diff --git a/app/src/main/java/com/m57/hermescontrol/ui/bots/group/GroupChatViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/bots/group/GroupChatViewModel.kt index 4b0157b7..22d407f2 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/bots/group/GroupChatViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/bots/group/GroupChatViewModel.kt @@ -52,6 +52,10 @@ data class MemberSession( const val DEFAULT_MAX_BOT_MESSAGES = 6 const val DEFAULT_MAX_CONTINUATION_PASSES = 2 +const val MAX_ALLOWED_BOT_MESSAGES = 50 +const val MAX_ALLOWED_CONTINUATION_PASSES = 20 +const val WARN_MAX_BOT_MESSAGES = 20 +const val WARN_MAX_CONTINUATION_PASSES = 6 private const val LEASE_STEP_TTL_MS = 45_000L private const val TURN_TIMEOUT_MS = 45_000L private const val HISTORY_LIMIT = 10 @@ -424,8 +428,8 @@ class GroupChatViewModel( maxPasses: Int, systemPrompt: String? = null, ) { - val clampedMessages = maxMessages.coerceIn(1, 20) - val clampedPasses = maxPasses.coerceIn(0, 10) + val clampedMessages = maxMessages.coerceIn(1, MAX_ALLOWED_BOT_MESSAGES) + val clampedPasses = maxPasses.coerceIn(0, MAX_ALLOWED_CONTINUATION_PASSES) val cleanPrompt = systemPrompt?.trim()?.ifBlank { null } _uiState.update { diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 03f09da1..6a82b593 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -1181,6 +1181,10 @@ تعليمات / موجه الغرفة قواعد مخصصة، أو أسلوب، أو سياق لهذه المجموعة (اختياري)… إرشادات مخصصة لجميع البوتات في هذه الغرفة +تحذير من الحدود المرتفعة +قد يؤدي تعيين حدود مرتفعة للرسائل (%1$d) أو التمريرات (%2$d) إلى استهلاك كبير للرموز (Tokens) وحدوث حلقات نقاش طويلة ومستمرة بين البوتات. +حفظ على أي حال +تعديل الحدود الإعدادات تم إيقاف النقاش إيقاف التوليد diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index a1e47513..9dd5cf74 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -863,6 +863,10 @@ 방 지침 / 프롬프트 이 그룹을 위한 사용자 지정 규칙, 어조 또는 컨텍스트 (선택사항)… 이 방의 모든 봇에 적용되는 사용자 지정 가이드라인 + 높은 제한 경고 + 메시지 수(%1$d) 또는 전달 횟수(%2$d)를 높게 설정하면 API 토큰이 대량 소비되고 봇 간의 긴 토론 루프가 발생할 수 있습니다. + 그래도 저장 + 제한 조정 설정 에이전트 계획 실시간 기록 diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 0912dbd7..bdff2ba1 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -1164,5 +1164,9 @@ 房间指令 / 提示词 此群聊的自定义规则、语气或上下文(可选)… 适用于此房间内所有 Bot 的自定义准则 + 高上限警告 + 设置较高的消息数 (%1$d) 或交接次数 (%2$d) 可能会消耗大量 API 令牌,并导致机器人之间产生长时间的连续讨论循环。 + 仍要保存 + 调整限制 设置 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9ab998eb..a3113b56 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1295,6 +1295,10 @@ Room Instructions / Prompt Custom rules, tone, or context for this group (optional)… Custom guidelines for all bots in this room + High Limits Warning + Setting high message (%1$d) or handoff (%2$d) limits can consume significant API tokens and lead to long continuous bot discussion loops. + Save Anyway + Adjust Limits Settings Discussion stopped Stop generation diff --git a/app/src/test/java/com/m57/hermescontrol/ui/bots/group/GroupChatViewModelTest.kt b/app/src/test/java/com/m57/hermescontrol/ui/bots/group/GroupChatViewModelTest.kt index a8bc0116..97943a25 100644 --- a/app/src/test/java/com/m57/hermescontrol/ui/bots/group/GroupChatViewModelTest.kt +++ b/app/src/test/java/com/m57/hermescontrol/ui/bots/group/GroupChatViewModelTest.kt @@ -420,6 +420,33 @@ class GroupChatViewModelTest { assertEquals(15, viewModel.uiState.value.maxBotMessages) assertEquals(4, viewModel.uiState.value.maxContinuationPasses) assertEquals("Custom instructions for this group", viewModel.uiState.value.systemPrompt) + + // Extended limits within new range + viewModel.updateGroupLimits( + maxMessages = 45, + maxPasses = 18, + ) + testScheduler.runCurrent() + assertEquals(45, viewModel.uiState.value.maxBotMessages) + assertEquals(18, viewModel.uiState.value.maxContinuationPasses) + + // Exceeding ceiling clamps to MAX_ALLOWED_BOT_MESSAGES (50) and MAX_ALLOWED_CONTINUATION_PASSES (20) + viewModel.updateGroupLimits( + maxMessages = 999, + maxPasses = 999, + ) + testScheduler.runCurrent() + assertEquals(MAX_ALLOWED_BOT_MESSAGES, viewModel.uiState.value.maxBotMessages) + assertEquals(MAX_ALLOWED_CONTINUATION_PASSES, viewModel.uiState.value.maxContinuationPasses) + + // Under floor clamps to 1 and 0 + viewModel.updateGroupLimits( + maxMessages = -10, + maxPasses = -5, + ) + testScheduler.runCurrent() + assertEquals(1, viewModel.uiState.value.maxBotMessages) + assertEquals(0, viewModel.uiState.value.maxContinuationPasses) } @Test