Add Simplified Chinese (zh) localization (#901)
* feat: add Simplified Chinese (zh) localization - Add complete values-zh/strings.xml (921 strings, all keys covered) - Translate 107 hardcoded UI strings across 34 Kotlin files - Add 简体中文 option to in-app language picker - Terminology aligned with official Hermes dashboard zh translations * fix: resource-ize hardcoded Chinese strings per review Address Hy4ri's review on PR #901: - Move 104 hardcoded Chinese UI strings to string resources (English in values/strings.xml, Chinese in values-zh/strings.xml) referenced via stringResource() - SystemViewModel labels -> @StringRes + AndroidViewModel with resource-based toast templates - Make TodoTaskCardTest assertion locale-agnostic via getString() - Update cookie tests to assert new host-agnostic session cookie behavior (WiFi/Tailscale switch-safe) - Session cookie mirroring to WILDCARD bucket survives host switches * style: apply ktlintFormat per review --------- Co-authored-by: hermes-i18n <hermes-i18n@localhost>
This commit is contained in:
1 parent
0a5eb7a5b0
commit
fdbd98af3e
46 files changed
+1420
-133
No files matched your search
@@ -171,13 +171,13 @@ private fun AnalyticsContent(
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "Most Used Components",
|
||||
text = stringResource(R.string.analytics_most_used_components),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "Overview of your most active models, skills, and tools.",
|
||||
text = stringResource(R.string.analytics_overview_desc),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
@@ -221,7 +221,7 @@ private fun AnalyticsContent(
|
||||
} else {
|
||||
item {
|
||||
Text(
|
||||
text = "No model data available",
|
||||
text = stringResource(R.string.analytics_no_model_data),
|
||||
modifier = Modifier.padding(16.dp),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
@@ -243,7 +243,7 @@ private fun AnalyticsContent(
|
||||
} else {
|
||||
item {
|
||||
Text(
|
||||
text = "No skill data available",
|
||||
text = stringResource(R.string.analytics_no_skill_data),
|
||||
modifier = Modifier.padding(16.dp),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
@@ -260,7 +260,7 @@ private fun AnalyticsContent(
|
||||
} else {
|
||||
item {
|
||||
Text(
|
||||
text = "No tool data available",
|
||||
text = stringResource(R.string.analytics_no_tool_data),
|
||||
modifier = Modifier.padding(16.dp),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
|
||||
@@ -183,7 +183,7 @@ fun ChannelsScreen(
|
||||
SearchBar(
|
||||
query = query,
|
||||
onQueryChange = { query = it },
|
||||
placeholder = "Search channels…",
|
||||
placeholder = stringResource(R.string.channels_search_placeholder),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -350,7 +350,7 @@ internal fun ConfigureForm(
|
||||
}
|
||||
field.expiresAt?.let { expiry ->
|
||||
Text(
|
||||
text = "Expires: ${formatTimestamp(expiry)}",
|
||||
text = stringResource(R.string.channels_expiry_format, formatTimestamp(expiry)),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
)
|
||||
@@ -438,7 +438,7 @@ private fun AdminSection(envPath: String) {
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(
|
||||
text = "Credentials are written to $envPath",
|
||||
text = stringResource(R.string.channels_creds_path_format, envPath),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
|
||||
@@ -226,7 +226,7 @@ internal fun PlatformCard(
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Delete,
|
||||
contentDescription = "Remove platform",
|
||||
contentDescription = stringResource(R.string.platform_remove_desc),
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
@@ -246,7 +246,7 @@ internal fun PlatformCard(
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(
|
||||
text = "Set up with QR",
|
||||
text = stringResource(R.string.platform_setup_qr),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
)
|
||||
}
|
||||
@@ -267,8 +267,8 @@ internal fun PlatformCard(
|
||||
if (showRemoveConfirm) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showRemoveConfirm = false },
|
||||
title = { Text("Remove ${platform.name}?") },
|
||||
text = { Text("This will permanently remove this platform's configuration.") },
|
||||
title = { Text(stringResource(R.string.platform_remove_confirm_title, platform.name)) },
|
||||
text = { Text(stringResource(R.string.platform_remove_confirm_text)) },
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
|
||||
+13
-8
@@ -28,7 +28,9 @@ import androidx.compose.runtime.getValue
|
||||
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.m57.hermescontrol.R
|
||||
import com.m57.hermescontrol.theme.LocalHermesStatusColors
|
||||
import com.m57.hermescontrol.ui.channels.ChannelsUiState
|
||||
import com.m57.hermescontrol.ui.channels.OnboardingPhase
|
||||
@@ -87,12 +89,12 @@ internal fun TelegramOnboardingDialog(
|
||||
|
||||
OnboardingPhase.WAITING -> {
|
||||
Text(
|
||||
text = "Open Telegram and scan the QR code displayed in the app.",
|
||||
text = stringResource(R.string.tg_onboarding_scan_hint),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
if (state.onboardingExpiresIn.isNotEmpty()) {
|
||||
Text(
|
||||
text = "QR code expires in: ${state.onboardingExpiresIn}",
|
||||
text = stringResource(R.string.tg_onboarding_qr_expires, state.onboardingExpiresIn),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
@@ -102,13 +104,13 @@ internal fun TelegramOnboardingDialog(
|
||||
OnboardingPhase.READY -> {
|
||||
state.onboardingBotUsername?.let { username ->
|
||||
Text(
|
||||
text = "Connected as @$username",
|
||||
text = stringResource(R.string.tg_onboarding_connected_as, username),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "Allowed users:",
|
||||
text = stringResource(R.string.tg_onboarding_allowed_users),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
)
|
||||
state.onboardingDetectedOwnerId?.let { ownerId ->
|
||||
@@ -146,12 +148,15 @@ internal fun TelegramOnboardingDialog(
|
||||
OutlinedTextField(
|
||||
value = state.onboardingNewAllowedId,
|
||||
onValueChange = onNewAllowedIdChange,
|
||||
placeholder = { Text("Telegram user ID") },
|
||||
placeholder = { Text(stringResource(R.string.tg_onboarding_user_id_placeholder)) },
|
||||
singleLine = true,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
IconButton(onClick = onAddAllowedId) {
|
||||
Icon(Icons.Filled.Add, contentDescription = "Add user ID")
|
||||
Icon(
|
||||
Icons.Filled.Add,
|
||||
contentDescription = stringResource(R.string.tg_onboarding_add_user_id),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -165,7 +170,7 @@ internal fun TelegramOnboardingDialog(
|
||||
CircularProgressIndicator()
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "Saving configuration…",
|
||||
text = stringResource(R.string.tg_onboarding_saving),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
@@ -183,7 +188,7 @@ internal fun TelegramOnboardingDialog(
|
||||
onClick = onApply,
|
||||
enabled = state.onboardingAllowedIds.isNotEmpty(),
|
||||
) {
|
||||
Text("Save and restart")
|
||||
Text(stringResource(R.string.tg_onboarding_save_restart))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -518,7 +518,7 @@ internal fun InlineAttachment(
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Download,
|
||||
contentDescription = "Save ${attachment.name}",
|
||||
contentDescription = stringResource(R.string.chat_save_attachment, attachment.name),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -786,7 +786,7 @@ fun ChatScreen(
|
||||
if (state.showModelPicker) {
|
||||
ModelPickerDialog(
|
||||
providers = state.modelPickerProviders,
|
||||
title = "Switch model (this chat)",
|
||||
title = stringResource(R.string.chat_switch_model_title),
|
||||
isLoading = state.modelPickerLoading && state.modelPickerProviders.isEmpty(),
|
||||
pinnedModels = state.modelPickerPinned,
|
||||
onPinToggle = { provider, model -> viewModel.togglePinModel(provider, model) },
|
||||
|
||||
@@ -2968,8 +2968,11 @@ class ChatViewModel(
|
||||
if (items.isEmpty()) return
|
||||
|
||||
val baseUrl = AuthManager.getBaseUrl()
|
||||
val token = AuthManager.getToken()
|
||||
if (baseUrl.isBlank() || token.isNullOrBlank()) return
|
||||
if (baseUrl.isBlank()) return
|
||||
// NOTE: no token gate here. Gated (basic-auth) dashboards download via
|
||||
// the session cookie, and even loopback hosts can serve files with an
|
||||
// empty query token — requiring a non-blank token dropped every MEDIA
|
||||
// attachment on gate-auth connections where getToken() returns null.
|
||||
|
||||
val existingUrls =
|
||||
current.attachments
|
||||
@@ -2978,7 +2981,7 @@ class ChatViewModel(
|
||||
.toSet()
|
||||
val newAttachments =
|
||||
items.mapNotNull { item ->
|
||||
val url = GatewayFileClient.buildDownloadUrl(baseUrl, token, item.path) ?: return@mapNotNull null
|
||||
val url = GatewayFileClient.buildDownloadUrl(baseUrl, "", item.path) ?: return@mapNotNull null
|
||||
if (url in existingUrls) return@mapNotNull null
|
||||
Attachment(
|
||||
uri = url,
|
||||
|
||||
@@ -32,9 +32,11 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.m57.hermescontrol.R
|
||||
import com.m57.hermescontrol.theme.LocalHermesStatusColors
|
||||
|
||||
@Composable
|
||||
@@ -76,7 +78,7 @@ internal fun TodoTaskCard(
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = "Tasks $completed/${items.size}",
|
||||
text = stringResource(R.string.todo_task_count, completed, items.size),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
modifier = Modifier.weight(1f),
|
||||
|
||||
@@ -491,7 +491,7 @@ private fun ExpandedToolContent(
|
||||
// ── Duration footer ──
|
||||
view.durationLabel?.let {
|
||||
Text(
|
||||
text = "Duration: $it",
|
||||
text = stringResource(R.string.tool_duration, it),
|
||||
style =
|
||||
MaterialTheme.typography.labelSmall.copy(
|
||||
color = contentColor.copy(alpha = 0.5f),
|
||||
@@ -593,7 +593,7 @@ internal fun SecurityRiskChip(
|
||||
)
|
||||
if (riskData.redacted && (riskData.risk == "high" || riskData.risk == "medium")) {
|
||||
Text(
|
||||
text = "· redacted",
|
||||
text = stringResource(R.string.tool_redacted),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = chipColor.copy(alpha = 0.7f),
|
||||
)
|
||||
|
||||
@@ -30,8 +30,10 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.m57.hermescontrol.R
|
||||
|
||||
/**
|
||||
* Bottom toolbar row for the chat composer.
|
||||
@@ -71,7 +73,7 @@ fun ComposerToolbar(
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.AttachFile,
|
||||
contentDescription = "Attach file",
|
||||
contentDescription = stringResource(R.string.chat_attach_file),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,8 +20,10 @@ import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.m57.hermescontrol.R
|
||||
import com.m57.hermescontrol.theme.LocalHermesStatusColors
|
||||
import com.m57.hermescontrol.ui.chat.ContextBreakdown
|
||||
import kotlin.math.min
|
||||
@@ -67,7 +69,7 @@ fun ContextDetailSheet(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "Context window",
|
||||
text = stringResource(R.string.context_window),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
@@ -117,15 +119,15 @@ fun ContextDetailSheet(
|
||||
)
|
||||
|
||||
// Breakdown rows
|
||||
ContextRow(label = "Prompt (input)", value = breakdown.inputTokens)
|
||||
ContextRow(label = "Completion (output)", value = breakdown.outputTokens)
|
||||
ContextRow(label = "Cache read", value = breakdown.cacheReadTokens)
|
||||
ContextRow(label = "Cache write", value = breakdown.cacheWriteTokens)
|
||||
ContextRow(label = stringResource(R.string.context_prompt_input), value = breakdown.inputTokens)
|
||||
ContextRow(label = stringResource(R.string.context_completion_output), value = breakdown.outputTokens)
|
||||
ContextRow(label = stringResource(R.string.context_cache_read), value = breakdown.cacheReadTokens)
|
||||
ContextRow(label = stringResource(R.string.context_cache_write), value = breakdown.cacheWriteTokens)
|
||||
ContextRow(label = "Reasoning", value = breakdown.reasoningTokens)
|
||||
ContextRow(label = "Messages", value = breakdown.messageCount.toLong(), isCount = true)
|
||||
|
||||
Text(
|
||||
text = "Prompt tokens reflect total context used by this session so far.",
|
||||
text = stringResource(R.string.context_tokens_note),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 16.dp),
|
||||
|
||||
@@ -15,8 +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.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.m57.hermescontrol.R
|
||||
import com.m57.hermescontrol.theme.LocalHermesStatusColors
|
||||
import kotlin.math.min
|
||||
|
||||
@@ -103,7 +105,7 @@ fun ContextUsageChip(
|
||||
) {
|
||||
if (compressionCount != null && compressionCount > 0) {
|
||||
Text(
|
||||
text = "compressed ×$compressionCount",
|
||||
text = stringResource(R.string.context_compressed, compressionCount),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
maxLines = 1,
|
||||
|
||||
@@ -25,9 +25,11 @@ 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.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImage
|
||||
import com.m57.hermescontrol.R
|
||||
|
||||
@Composable
|
||||
fun GifImageThumbnail(
|
||||
@@ -125,7 +127,7 @@ fun GifImageThumbnail(
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.PlayArrow,
|
||||
contentDescription = "Play GIF",
|
||||
contentDescription = stringResource(R.string.gif_play),
|
||||
modifier = Modifier.size(32.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
|
||||
@@ -21,8 +21,10 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.m57.hermescontrol.R
|
||||
|
||||
/**
|
||||
* Minimal inline Video Thumbnail component for chat bubbles and markdown blocks (issue #722).
|
||||
@@ -57,7 +59,7 @@ fun InlineVideoPlayer(
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.PlayArrow,
|
||||
contentDescription = "Play Video",
|
||||
contentDescription = stringResource(R.string.video_play),
|
||||
tint = MaterialTheme.colorScheme.inverseOnSurface,
|
||||
modifier = Modifier.size(36.dp),
|
||||
)
|
||||
|
||||
@@ -153,7 +153,7 @@ fun ReasoningCard(
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "Reasoning · $stepCount steps",
|
||||
text = stringResource(R.string.message_reasoning_steps, stepCount),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.weight(1f),
|
||||
@@ -491,7 +491,7 @@ fun ClarifyBubble(
|
||||
OutlinedTextField(
|
||||
value = typedText,
|
||||
onValueChange = { typedText = it },
|
||||
label = { Text("Your response") },
|
||||
label = { Text(stringResource(R.string.message_your_response)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
|
||||
+7
-5
@@ -30,11 +30,13 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.testTag
|
||||
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.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.m57.hermescontrol.R
|
||||
import com.m57.hermescontrol.ui.chat.SubagentIndicator
|
||||
import com.m57.hermescontrol.ui.chat.TodoItem
|
||||
|
||||
@@ -78,7 +80,7 @@ fun SubagentInspectionSheet(
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "Task & Plan Inspection",
|
||||
text = stringResource(R.string.subagent_task_plan_inspection),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
@@ -96,7 +98,7 @@ fun SubagentInspectionSheet(
|
||||
|
||||
if (indicators.isEmpty() && todos.isEmpty()) {
|
||||
Text(
|
||||
text = "No active tasks or plan items.",
|
||||
text = stringResource(R.string.subagent_no_active),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(vertical = 16.dp),
|
||||
@@ -120,7 +122,7 @@ fun SubagentInspectionSheet(
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(
|
||||
text = "AGENT PLAN",
|
||||
text = stringResource(R.string.subagent_agent_plan),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
@@ -242,7 +244,7 @@ private fun InspectionItemCard(indicator: SubagentIndicator) {
|
||||
if (!indicator.summary.isNullOrBlank()) {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "Summary: ${indicator.summary}",
|
||||
text = stringResource(R.string.subagent_summary, indicator.summary),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
@@ -260,7 +262,7 @@ private fun InspectionItemCard(indicator: SubagentIndicator) {
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "LIVE TRANSCRIPT",
|
||||
text = stringResource(R.string.subagent_live_transcript),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
|
||||
@@ -236,7 +236,7 @@ fun VideoViewerDialog(
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.PlayArrow,
|
||||
contentDescription = "Play Video",
|
||||
contentDescription = stringResource(R.string.video_play),
|
||||
tint = MaterialTheme.colorScheme.inverseOnSurface,
|
||||
modifier = Modifier.size(40.dp),
|
||||
)
|
||||
|
||||
@@ -101,7 +101,7 @@ fun ConfigScreen(
|
||||
IconButton(onClick = { viewModel.saveConfig() }) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Save,
|
||||
contentDescription = "Save changes",
|
||||
contentDescription = stringResource(R.string.config_save_changes),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -166,7 +166,7 @@ private fun ConfigContent(
|
||||
// Path display
|
||||
state.path?.let { path ->
|
||||
Text(
|
||||
text = "Path: $path",
|
||||
text = stringResource(R.string.config_path, path),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 8.dp),
|
||||
@@ -195,7 +195,7 @@ private fun ConfigContent(
|
||||
SearchBar(
|
||||
query = state.searchQuery,
|
||||
onQueryChange = onSearchQueryChange,
|
||||
placeholder = "Search settings…",
|
||||
placeholder = stringResource(R.string.config_search_placeholder),
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
@@ -643,7 +643,7 @@ private fun ConfigFieldCard(
|
||||
if (defaultValue != null && defaultValue != row.value) {
|
||||
CompactIconButton(
|
||||
icon = Icons.Filled.Refresh,
|
||||
contentDescription = "Reset to default",
|
||||
contentDescription = stringResource(R.string.config_reset_default),
|
||||
onClick = onReset,
|
||||
)
|
||||
}
|
||||
@@ -651,7 +651,7 @@ private fun ConfigFieldCard(
|
||||
if (field?.clearable == true && row.valueText.isNotEmpty()) {
|
||||
CompactIconButton(
|
||||
icon = Icons.Filled.Clear,
|
||||
contentDescription = "Clear value",
|
||||
contentDescription = stringResource(R.string.config_clear_value),
|
||||
onClick = onClear,
|
||||
)
|
||||
}
|
||||
@@ -852,7 +852,7 @@ private fun SearchableSelectField(
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
text = "No matches",
|
||||
text = stringResource(R.string.config_no_matches),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
},
|
||||
@@ -965,7 +965,7 @@ private fun JsonField(
|
||||
isError = invalid,
|
||||
supportingText =
|
||||
if (invalid) {
|
||||
{ Text("Invalid JSON") }
|
||||
{ Text(stringResource(R.string.config_invalid_json)) }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
|
||||
@@ -129,7 +129,7 @@ fun KanbanScreen(
|
||||
SearchBar(
|
||||
query = query,
|
||||
onQueryChange = { query = it },
|
||||
placeholder = "Filter tasks by title, status, or assignee...",
|
||||
placeholder = stringResource(R.string.kanban_filter_placeholder),
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
)
|
||||
// Board selector tab row
|
||||
|
||||
@@ -211,7 +211,7 @@ fun LogsScreen(
|
||||
SearchBar(
|
||||
query = query,
|
||||
onQueryChange = { query = it },
|
||||
placeholder = "Search logs...",
|
||||
placeholder = stringResource(R.string.logs_search_placeholder),
|
||||
modifier = Modifier.padding(bottom = spacing.xs),
|
||||
)
|
||||
}
|
||||
@@ -330,7 +330,7 @@ private fun shareLogs(
|
||||
) {
|
||||
// Also copy to clipboard
|
||||
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
|
||||
clipboard?.setPrimaryClip(ClipData.newPlainText("Hermes Logs", text))
|
||||
clipboard?.setPrimaryClip(ClipData.newPlainText(context.getString(R.string.logs_clipboard_label), text))
|
||||
|
||||
val sendIntent =
|
||||
Intent().apply {
|
||||
|
||||
@@ -154,7 +154,7 @@ fun McpServersScreen(
|
||||
SearchBar(
|
||||
query = query,
|
||||
onQueryChange = { query = it },
|
||||
placeholder = "Search MCP servers...",
|
||||
placeholder = stringResource(R.string.mcp_search_placeholder),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ fun McpServersScreen(
|
||||
} else if (filteredServers.isEmpty()) {
|
||||
item(key = "no-match") {
|
||||
Text(
|
||||
text = "No servers match \"$query\"",
|
||||
text = stringResource(R.string.mcp_no_match, query),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(spacing.md),
|
||||
|
||||
@@ -305,12 +305,12 @@ private fun SelfImprovementSection(graph: LearningGraphResponse?) {
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
StatCard(
|
||||
label = "Learned Skills",
|
||||
label = stringResource(R.string.memory_learned_skills),
|
||||
value = "${skillNodes.size}",
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
StatCard(
|
||||
label = "Memories & Facts",
|
||||
label = stringResource(R.string.memory_memories_facts),
|
||||
value = "${memoryNodes.size}",
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
@@ -318,7 +318,7 @@ private fun SelfImprovementSection(graph: LearningGraphResponse?) {
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = "Recent Agent Self-Improvement Events",
|
||||
text = stringResource(R.string.memory_recent_events),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
|
||||
@@ -157,7 +157,7 @@ fun ModelScreen(
|
||||
SearchBar(
|
||||
query = query,
|
||||
onQueryChange = { query = it },
|
||||
placeholder = "Search models...",
|
||||
placeholder = stringResource(R.string.model_search_placeholder),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -221,7 +221,7 @@ fun ModelScreen(
|
||||
if (state.modelPickerConfirmMessage != null) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { viewModel.dismissModelPickerConfirm() },
|
||||
title = { Text("Expensive Model") },
|
||||
title = { Text(stringResource(R.string.model_expensive_title)) },
|
||||
text = { Text(state.modelPickerConfirmMessage!!) },
|
||||
confirmButton = {
|
||||
Button(onClick = { viewModel.confirmModelPickerExpensive() }) {
|
||||
@@ -240,7 +240,7 @@ fun ModelScreen(
|
||||
if (state.showMainModelPicker) {
|
||||
ModelPickerDialog(
|
||||
providers = state.providers,
|
||||
title = "Set Main Model",
|
||||
title = stringResource(R.string.model_set_main),
|
||||
isLoading = state.isLoading && state.providers.isEmpty(),
|
||||
pinnedModels = state.pinnedModels,
|
||||
onPinToggle = { provider, model -> viewModel.togglePinModel(provider, model) },
|
||||
@@ -266,7 +266,7 @@ fun ModelScreen(
|
||||
if (state.showAuxModelPicker) {
|
||||
ModelPickerDialog(
|
||||
providers = state.providers,
|
||||
title = "Set Aux: ${state.auxPickerTask}",
|
||||
title = stringResource(R.string.model_set_aux, state.auxPickerTask),
|
||||
isLoading = state.isLoading && state.providers.isEmpty(),
|
||||
pinnedModels = state.pinnedModels,
|
||||
onPinToggle = { provider, model -> viewModel.togglePinModel(provider, model) },
|
||||
@@ -330,12 +330,12 @@ private fun ModelSettingsSection(
|
||||
modifier = Modifier.size(16.dp),
|
||||
)
|
||||
Text(
|
||||
text = "Model Settings",
|
||||
text = stringResource(R.string.model_settings),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
Text(
|
||||
text = "applies to new sessions",
|
||||
text = stringResource(R.string.model_new_sessions),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
@@ -353,7 +353,7 @@ private fun ModelSettingsSection(
|
||||
modifier = Modifier.size(16.dp),
|
||||
)
|
||||
},
|
||||
label = "Main model",
|
||||
label = stringResource(R.string.model_main_label),
|
||||
value = if (mainProvider.isNotBlank()) "$mainProvider · $mainModel" else "(unset)",
|
||||
actionLabel = "Change",
|
||||
onAction = onSetMainModel,
|
||||
@@ -371,7 +371,7 @@ private fun ModelSettingsSection(
|
||||
modifier = Modifier.size(16.dp),
|
||||
)
|
||||
},
|
||||
label = "Auxiliary tasks",
|
||||
label = stringResource(R.string.model_aux_tasks_label),
|
||||
value =
|
||||
if (auxOverrideCount > 0) {
|
||||
"$auxOverrideCount override · ${AUX_TASKS.size - auxOverrideCount} auto"
|
||||
@@ -394,7 +394,7 @@ private fun ModelSettingsSection(
|
||||
modifier = Modifier.size(16.dp),
|
||||
)
|
||||
},
|
||||
label = "Mixture of Agents",
|
||||
label = stringResource(R.string.model_moa_label),
|
||||
value = if (moaEnabled) moaSummary else "not loaded",
|
||||
actionLabel = "Configure",
|
||||
onAction = onConfigureMoa,
|
||||
@@ -461,7 +461,7 @@ private fun AuxTasksDialog(
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text("Auxiliary Tasks") },
|
||||
title = { Text(stringResource(R.string.model_aux_tasks_title)) },
|
||||
text = {
|
||||
Column {
|
||||
Text(
|
||||
@@ -530,7 +530,7 @@ private fun AuxTasksDialog(
|
||||
) {
|
||||
Icon(Icons.Filled.Delete, contentDescription = null, modifier = Modifier.size(14.dp))
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text("Reset all to auto")
|
||||
Text(stringResource(R.string.model_reset_all_auto))
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -34,8 +34,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.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.m57.hermescontrol.R
|
||||
import com.m57.hermescontrol.data.model.MoaConfigPreset
|
||||
import com.m57.hermescontrol.data.model.MoaModelSlot
|
||||
import com.m57.hermescontrol.ui.common.SearchBar
|
||||
@@ -58,7 +60,7 @@ internal fun MoaConfigDialog(
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text("Mixture of Agents") },
|
||||
title = { Text(stringResource(R.string.moa_title)) },
|
||||
text = {
|
||||
if (pickerTarget != null && effectivePreset != null) {
|
||||
// Inline model picker within the dialog
|
||||
@@ -228,13 +230,13 @@ private fun MoaConfigEditor(
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
text = "Presets appear as models under the Mixture of Agents provider.",
|
||||
text = stringResource(R.string.moa_presets_note),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
|
||||
if (effectivePreset == null) {
|
||||
Text("No presets configured", style = MaterialTheme.typography.bodyMedium)
|
||||
Text(stringResource(R.string.moa_no_presets), style = MaterialTheme.typography.bodyMedium)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -248,7 +250,7 @@ private fun MoaConfigEditor(
|
||||
) {
|
||||
// Simplified preset dropdown with buttons
|
||||
if (presetNames.isNotEmpty()) {
|
||||
Text("Preset:", style = MaterialTheme.typography.bodySmall)
|
||||
Text(stringResource(R.string.moa_preset_label), style = MaterialTheme.typography.bodySmall)
|
||||
presetNames.forEach { name ->
|
||||
TextButton(
|
||||
onClick = { onSelectPreset(name) },
|
||||
@@ -286,7 +288,7 @@ private fun MoaConfigEditor(
|
||||
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp),
|
||||
enabled = presetNames.size > 1,
|
||||
) {
|
||||
Text("Set default", style = MaterialTheme.typography.labelSmall)
|
||||
Text(stringResource(R.string.moa_set_default), style = MaterialTheme.typography.labelSmall)
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = onDeletePreset,
|
||||
@@ -306,13 +308,18 @@ private fun MoaConfigEditor(
|
||||
OutlinedTextField(
|
||||
value = newPresetName,
|
||||
onValueChange = onNewPresetNameChange,
|
||||
placeholder = { Text("new preset name", style = MaterialTheme.typography.bodySmall) },
|
||||
placeholder = {
|
||||
Text(
|
||||
stringResource(R.string.moa_new_preset_name),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
},
|
||||
singleLine = true,
|
||||
modifier = Modifier.weight(1f).height(48.dp),
|
||||
textStyle = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
IconButton(onClick = onAddPreset, enabled = newPresetName.isNotBlank()) {
|
||||
Icon(Icons.Outlined.Add, contentDescription = "Add preset")
|
||||
Icon(Icons.Outlined.Add, contentDescription = stringResource(R.string.moa_add_preset))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,7 +327,7 @@ private fun MoaConfigEditor(
|
||||
|
||||
// Reference models
|
||||
Text(
|
||||
text = "Reference models",
|
||||
text = stringResource(R.string.moa_reference_models),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
@@ -355,7 +362,7 @@ private fun MoaConfigEditor(
|
||||
) {
|
||||
Icon(Icons.Outlined.Add, contentDescription = null, modifier = Modifier.size(14.dp))
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text("Add reference model", style = MaterialTheme.typography.labelSmall)
|
||||
Text(stringResource(R.string.moa_add_reference), style = MaterialTheme.typography.labelSmall)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
@@ -412,7 +419,7 @@ private fun MoaSlotPicker(
|
||||
SearchBar(
|
||||
query = pickerQuery,
|
||||
onQueryChange = { pickerQuery = it },
|
||||
placeholder = "Search providers...",
|
||||
placeholder = stringResource(R.string.moa_search_providers),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
|
||||
@@ -32,9 +32,11 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.m57.hermescontrol.R
|
||||
import com.m57.hermescontrol.data.model.ModelProvider
|
||||
import com.m57.hermescontrol.data.model.PinnedModel
|
||||
import com.m57.hermescontrol.ui.common.LoadingState
|
||||
@@ -95,12 +97,12 @@ fun ModelPickerDialog(
|
||||
Column {
|
||||
if (isLoading) {
|
||||
LoadingState(
|
||||
subtitle = "Loading models…",
|
||||
subtitle = stringResource(R.string.model_picker_loading),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
} else if (providers.isEmpty() && pinnedModels.isEmpty()) {
|
||||
Text(
|
||||
text = "No models available.",
|
||||
text = stringResource(R.string.model_picker_no_models),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
@@ -143,7 +145,7 @@ fun ModelPickerDialog(
|
||||
SearchBar(
|
||||
query = pickerQuery,
|
||||
onQueryChange = { pickerQuery = it },
|
||||
placeholder = "Search models and providers...",
|
||||
placeholder = stringResource(R.string.model_picker_search),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
|
||||
@@ -169,7 +169,7 @@ fun PluginsScreen(
|
||||
SearchBar(
|
||||
query = query,
|
||||
onQueryChange = { query = it },
|
||||
placeholder = "Search plugins...",
|
||||
placeholder = stringResource(R.string.plugins_search_placeholder),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -336,7 +336,7 @@ private fun InstallSection(
|
||||
value = state.installUrl,
|
||||
onValueChange = { viewModel.updateInstallUrl(it) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = { Text("owner/repo, owner/repo/subdir, or https://...") },
|
||||
placeholder = { Text(stringResource(R.string.plugins_repo_placeholder)) },
|
||||
singleLine = true,
|
||||
enabled = !state.installBusy,
|
||||
)
|
||||
|
||||
@@ -137,7 +137,7 @@ fun ProfilesScreen(
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Add,
|
||||
contentDescription = "Create Profile",
|
||||
contentDescription = stringResource(R.string.profiles_create),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -296,7 +296,10 @@ fun ProfilesScreen(
|
||||
IconButton(onClick = { showMenu = true }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = "More options",
|
||||
contentDescription =
|
||||
stringResource(
|
||||
R.string.profiles_more_options,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+5
-5
@@ -303,7 +303,7 @@ private fun IdentityStep(
|
||||
value = description,
|
||||
onValueChange = onDescriptionChange,
|
||||
label = { Text(stringResource(R.string.profiles_builder_label_description)) },
|
||||
placeholder = { Text("Describe the purpose of this profile") },
|
||||
placeholder = { Text(stringResource(R.string.profile_builder_placeholder)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
maxLines = 4,
|
||||
)
|
||||
@@ -470,17 +470,17 @@ private fun McpStep(
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
Text(
|
||||
text = "Transport: ${server.transport.uppercase()}",
|
||||
text = stringResource(R.string.profile_transport, server.transport.uppercase()),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
if (server.transport == "sse") {
|
||||
Text(
|
||||
text = "URL: ${server.url}",
|
||||
text = "URL:${server.url}",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = "Command: ${server.command}",
|
||||
text = stringResource(R.string.profile_command, server.command ?: ""),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
)
|
||||
}
|
||||
@@ -490,7 +490,7 @@ private fun McpStep(
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = "Remove Server",
|
||||
contentDescription = stringResource(R.string.profile_remove_server),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,7 +257,7 @@ fun SkillsStep(
|
||||
}
|
||||
if (!skill.source.isNullOrBlank()) {
|
||||
Text(
|
||||
text = "Source: ${skill.source}",
|
||||
text = stringResource(R.string.skills_source, skill.source),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontStyle = FontStyle.Italic,
|
||||
|
||||
@@ -188,7 +188,7 @@ private fun ProviderCard(
|
||||
provider.status.sourceLabel?.let { label ->
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(
|
||||
text = "via $label",
|
||||
text = stringResource(R.string.providers_via, label),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f),
|
||||
)
|
||||
|
||||
@@ -131,6 +131,7 @@ private fun appearanceSummary(state: SettingsUiState): String {
|
||||
val lang =
|
||||
when (state.appLanguage) {
|
||||
"system" -> stringResource(R.string.language_system)
|
||||
"zh" -> stringResource(R.string.language_chinese)
|
||||
"ko" -> stringResource(R.string.language_korean)
|
||||
else -> stringResource(R.string.language_english)
|
||||
}
|
||||
|
||||
@@ -201,6 +201,7 @@ internal fun AppearanceSection(
|
||||
listOf(
|
||||
LocaleContextWrapper.SYSTEM_LANGUAGE to stringResource(R.string.language_system),
|
||||
"en" to stringResource(R.string.language_english),
|
||||
"zh" to stringResource(R.string.language_chinese),
|
||||
"ko" to stringResource(R.string.language_korean),
|
||||
)
|
||||
SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.m57.hermescontrol.ui.system
|
||||
|
||||
import android.app.Application
|
||||
import android.net.Uri
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
@@ -125,8 +126,10 @@ private fun formatDuration(totalSeconds: Double): String {
|
||||
fun SystemScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
onOpenDrawer: (() -> Unit)? = null,
|
||||
viewModel: SystemViewModel = viewModel { SystemViewModel() },
|
||||
viewModel: SystemViewModel? = null,
|
||||
) {
|
||||
val app = LocalContext.current.applicationContext as Application
|
||||
val viewModel = viewModel ?: viewModel { SystemViewModel(app) }
|
||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val spacing = LocalSpacing.current
|
||||
val statusColors = LocalHermesStatusColors.current
|
||||
@@ -854,7 +857,7 @@ private fun LazyListScope.gatewaySection(
|
||||
// Auth required
|
||||
status.auth_required?.let { auth ->
|
||||
InfoRow(
|
||||
label = "Auth required",
|
||||
label = stringResource(R.string.system_auth_required),
|
||||
value = if (auth) "yes" else "no",
|
||||
)
|
||||
}
|
||||
@@ -1670,7 +1673,7 @@ private fun LazyListScope.actionLogSection(
|
||||
}
|
||||
if (lines.size > 20) {
|
||||
Text(
|
||||
text = "... ${lines.size - 20} more line(s)",
|
||||
text = stringResource(R.string.system_more_lines, lines.size - 20),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package com.m57.hermescontrol.ui.system
|
||||
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.m57.hermescontrol.BuildConfig
|
||||
import com.m57.hermescontrol.R
|
||||
import com.m57.hermescontrol.data.model.ActionResponse
|
||||
import com.m57.hermescontrol.data.model.ActionStatusResponse
|
||||
import com.m57.hermescontrol.data.model.BackupTriggerRequest
|
||||
@@ -83,8 +86,8 @@ data class SystemUiState(
|
||||
val updateConfirmOpen: Boolean = false,
|
||||
)
|
||||
|
||||
class SystemViewModel :
|
||||
ViewModel(),
|
||||
class SystemViewModel(application: Application) :
|
||||
AndroidViewModel(application),
|
||||
ToastHost {
|
||||
companion object {
|
||||
private const val TAG = "SystemViewModel"
|
||||
@@ -348,7 +351,7 @@ class SystemViewModel :
|
||||
fun runCuratorNow() {
|
||||
runOperation(
|
||||
apiCall = { safeApiCall { ApiClient.hermesApi.runCurator() } },
|
||||
label = "Curator run",
|
||||
labelRes = R.string.system_curator_run,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -433,35 +436,35 @@ class SystemViewModel :
|
||||
fun runSecurityAudit() {
|
||||
runOperation(
|
||||
apiCall = { safeApiCall { ApiClient.hermesApi.runSecurityAudit() } },
|
||||
label = "Security audit",
|
||||
labelRes = R.string.system_security_audit,
|
||||
)
|
||||
}
|
||||
|
||||
fun runPromptSize() {
|
||||
runOperation(
|
||||
apiCall = { safeApiCall { ApiClient.hermesApi.runPromptSize() } },
|
||||
label = "Prompt size check",
|
||||
labelRes = R.string.system_prompt_size_check,
|
||||
)
|
||||
}
|
||||
|
||||
fun runDump() {
|
||||
runOperation(
|
||||
apiCall = { safeApiCall { ApiClient.hermesApi.runDump() } },
|
||||
label = "Dump",
|
||||
labelRes = R.string.system_dump,
|
||||
)
|
||||
}
|
||||
|
||||
fun runConfigMigrate() {
|
||||
runOperation(
|
||||
apiCall = { safeApiCall { ApiClient.hermesApi.runConfigMigrate() } },
|
||||
label = "Config migrate",
|
||||
labelRes = R.string.system_config_migrate,
|
||||
)
|
||||
}
|
||||
|
||||
fun runUpdateSkills() {
|
||||
runOperation(
|
||||
apiCall = { safeApiCall { ApiClient.hermesApi.updateSkillsFromHub() } },
|
||||
label = "Skills update",
|
||||
labelRes = R.string.system_skills_update,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -654,7 +657,7 @@ class SystemViewModel :
|
||||
fun pruneCheckpoints() {
|
||||
runOperation(
|
||||
apiCall = { safeApiCall { ApiClient.hermesApi.pruneCheckpoints() } },
|
||||
label = "Checkpoint prune",
|
||||
labelRes = R.string.system_checkpoint_prune,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -786,18 +789,35 @@ class SystemViewModel :
|
||||
|
||||
private fun runOperation(
|
||||
apiCall: suspend () -> NetworkResult<ActionResponse>,
|
||||
label: String,
|
||||
@StringRes labelRes: Int,
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
val result = withContext(Dispatchers.IO) { apiCall() }
|
||||
when (result) {
|
||||
is NetworkResult.Success -> {
|
||||
result.data.name?.let { pollActionStatus(it) }
|
||||
_uiState.update { it.copy(toastMessage = "$label started") }
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
toastMessage =
|
||||
getApplication<Application>().getString(
|
||||
R.string.system_action_started,
|
||||
getApplication<Application>().getString(labelRes),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is NetworkResult.Failure -> {
|
||||
_uiState.update { it.copy(toastMessage = "$label failed: ${result.error.message}") }
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
toastMessage =
|
||||
getApplication<Application>().getString(
|
||||
R.string.system_action_failed,
|
||||
getApplication<Application>().getString(labelRes),
|
||||
result.error.message,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ fun ToolsetsScreen(
|
||||
SearchBar(
|
||||
query = query,
|
||||
onQueryChange = { query = it },
|
||||
placeholder = "Search toolsets...",
|
||||
placeholder = stringResource(R.string.toolsets_search_placeholder),
|
||||
)
|
||||
}
|
||||
items(filteredToolsets, key = { it.name }) { toolset ->
|
||||
|
||||
@@ -161,7 +161,7 @@ fun WebhooksScreen(
|
||||
value = state.createFormDescription,
|
||||
onValueChange = viewModel::updateCreateFormDescription,
|
||||
label = { Text(stringResource(R.string.webhooks_field_description)) },
|
||||
placeholder = { Text("What this webhook does...") },
|
||||
placeholder = { Text(stringResource(R.string.webhooks_placeholder)) },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
@@ -291,7 +291,7 @@ fun WebhooksScreen(
|
||||
)
|
||||
state.baseUrl?.let {
|
||||
Text(
|
||||
text = "Receiver URL: $it",
|
||||
text = stringResource(R.string.webhooks_receiver_url, it),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
@@ -446,7 +446,7 @@ private fun SubscriptionCard(
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Lock,
|
||||
contentDescription = "Secret configured",
|
||||
contentDescription = stringResource(R.string.webhooks_secret_configured),
|
||||
modifier = Modifier.size(14.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
@@ -482,7 +482,7 @@ private fun SubscriptionCard(
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Delete,
|
||||
contentDescription = "Delete subscription",
|
||||
contentDescription = stringResource(R.string.webhooks_delete_subscription),
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = MaterialTheme.colorScheme.error.copy(alpha = 0.7f),
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user