fix(ui): collapse provider model pickers (#1126)
This commit is contained in:
@@ -2,7 +2,9 @@ package com.m57.hermescontrol.ui.model
|
|||||||
|
|
||||||
import androidx.compose.ui.test.assertIsDisplayed
|
import androidx.compose.ui.test.assertIsDisplayed
|
||||||
import androidx.compose.ui.test.junit4.createComposeRule
|
import androidx.compose.ui.test.junit4.createComposeRule
|
||||||
|
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||||
import androidx.compose.ui.test.onNodeWithText
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
|
import androidx.compose.ui.test.performClick
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.filters.MediumTest
|
import androidx.test.filters.MediumTest
|
||||||
import com.m57.hermescontrol.data.model.ModelProvider
|
import com.m57.hermescontrol.data.model.ModelProvider
|
||||||
@@ -62,6 +64,34 @@ class ModelScreenTest {
|
|||||||
composeTestRule.onNodeWithText("Pinned Models").assertDoesNotExist()
|
composeTestRule.onNodeWithText("Pinned Models").assertDoesNotExist()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun modelScreen_providerExpansionArrowMatchesState() {
|
||||||
|
uiStateFlow.value =
|
||||||
|
ModelUiState(
|
||||||
|
providers =
|
||||||
|
listOf(
|
||||||
|
ModelProvider(
|
||||||
|
slug = "openai",
|
||||||
|
name = "OpenAI",
|
||||||
|
models = listOf("gpt-4"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
composeTestRule.setContent {
|
||||||
|
ModelScreen(
|
||||||
|
onOpenDrawer = {},
|
||||||
|
viewModel = mockViewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithText("gpt-4").assertDoesNotExist()
|
||||||
|
composeTestRule.onNodeWithContentDescription("Expand OpenAI models").performClick()
|
||||||
|
composeTestRule.onNodeWithText("gpt-4").assertIsDisplayed()
|
||||||
|
composeTestRule.onNodeWithContentDescription("Collapse OpenAI models").performClick()
|
||||||
|
composeTestRule.onNodeWithText("gpt-4").assertDoesNotExist()
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun modelScreen_rendersPinnedModels_whenPresent() {
|
fun modelScreen_rendersPinnedModels_whenPresent() {
|
||||||
uiStateFlow.value =
|
uiStateFlow.value =
|
||||||
|
|||||||
+151
@@ -0,0 +1,151 @@
|
|||||||
|
package com.m57.hermescontrol.ui.model.components
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.assertCountEquals
|
||||||
|
import androidx.compose.ui.test.assertIsDisplayed
|
||||||
|
import androidx.compose.ui.test.hasSetTextAction
|
||||||
|
import androidx.compose.ui.test.junit4.createComposeRule
|
||||||
|
import androidx.compose.ui.test.onAllNodesWithText
|
||||||
|
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||||
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
|
import androidx.compose.ui.test.performClick
|
||||||
|
import androidx.compose.ui.test.performTextInput
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.filters.MediumTest
|
||||||
|
import com.m57.hermescontrol.data.model.ModelCapabilities
|
||||||
|
import com.m57.hermescontrol.data.model.ModelProvider
|
||||||
|
import com.m57.hermescontrol.data.model.PinnedModel
|
||||||
|
import com.m57.hermescontrol.theme.HermesControlTheme
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
@MediumTest
|
||||||
|
class ModelPickerDialogTest {
|
||||||
|
@get:Rule
|
||||||
|
val composeTestRule = createComposeRule()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun providers_startCollapsed_andOnlyOneProviderExpands() {
|
||||||
|
composeTestRule.setContent {
|
||||||
|
HermesControlTheme {
|
||||||
|
ModelPickerDialog(
|
||||||
|
providers =
|
||||||
|
listOf(
|
||||||
|
ModelProvider(slug = "openai", name = "OpenAI", models = listOf("gpt-4")),
|
||||||
|
ModelProvider(slug = "anthropic", name = "Anthropic", models = listOf("claude-3")),
|
||||||
|
),
|
||||||
|
title = "Choose model",
|
||||||
|
onSelect = { _, _ -> },
|
||||||
|
onDismiss = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithText("gpt-4").assertDoesNotExist()
|
||||||
|
composeTestRule.onNodeWithText("claude-3").assertDoesNotExist()
|
||||||
|
composeTestRule.onAllNodesWithText("1 model").assertCountEquals(2)
|
||||||
|
composeTestRule.onNodeWithContentDescription("Expand OpenAI models").assertIsDisplayed()
|
||||||
|
composeTestRule.onNodeWithContentDescription("Expand Anthropic models").assertIsDisplayed()
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithContentDescription("Expand OpenAI models").performClick()
|
||||||
|
composeTestRule.onNodeWithText("gpt-4").assertIsDisplayed()
|
||||||
|
composeTestRule.onNodeWithContentDescription("Collapse OpenAI models").assertIsDisplayed()
|
||||||
|
composeTestRule.onNodeWithText("claude-3").assertDoesNotExist()
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithContentDescription("Expand Anthropic models").performClick()
|
||||||
|
composeTestRule.onNodeWithText("gpt-4").assertDoesNotExist()
|
||||||
|
composeTestRule.onNodeWithText("claude-3").assertIsDisplayed()
|
||||||
|
composeTestRule.onNodeWithContentDescription("Collapse Anthropic models").assertIsDisplayed()
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithContentDescription("Collapse Anthropic models").performClick()
|
||||||
|
composeTestRule.onNodeWithText("claude-3").assertDoesNotExist()
|
||||||
|
composeTestRule.onNodeWithContentDescription("Expand Anthropic models").assertIsDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun search_filtersModelsAndCountsMatches_beforeSelection() {
|
||||||
|
var selectedProvider = ""
|
||||||
|
var selectedModel = ""
|
||||||
|
|
||||||
|
composeTestRule.setContent {
|
||||||
|
HermesControlTheme {
|
||||||
|
ModelPickerDialog(
|
||||||
|
providers =
|
||||||
|
listOf(
|
||||||
|
ModelProvider(
|
||||||
|
slug = "openai",
|
||||||
|
name = "OpenAI",
|
||||||
|
models = listOf("gpt-4", "embedding-3"),
|
||||||
|
),
|
||||||
|
ModelProvider(slug = "anthropic", name = "Anthropic", models = listOf("claude-3")),
|
||||||
|
),
|
||||||
|
title = "Choose model",
|
||||||
|
onSelect = { provider, model ->
|
||||||
|
selectedProvider = provider
|
||||||
|
selectedModel = model
|
||||||
|
},
|
||||||
|
onDismiss = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithText("2 models").assertIsDisplayed()
|
||||||
|
composeTestRule.onNode(hasSetTextAction()).performTextInput("gpt-4")
|
||||||
|
composeTestRule.onNodeWithText("1 model").assertIsDisplayed()
|
||||||
|
composeTestRule.onNodeWithText("embedding-3").assertDoesNotExist()
|
||||||
|
composeTestRule.onAllNodesWithText("gpt-4").assertCountEquals(1)
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithContentDescription("Expand OpenAI models").performClick()
|
||||||
|
composeTestRule.onAllNodesWithText("gpt-4").get(1).performClick()
|
||||||
|
|
||||||
|
assertEquals("openai", selectedProvider)
|
||||||
|
assertEquals("gpt-4", selectedModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun pinnedModels_remainVisibleAndPreserveCapabilitiesAndCallbacks() {
|
||||||
|
var toggledProvider = ""
|
||||||
|
var toggledModel = ""
|
||||||
|
var selectedProvider = ""
|
||||||
|
var selectedModel = ""
|
||||||
|
|
||||||
|
composeTestRule.setContent {
|
||||||
|
HermesControlTheme {
|
||||||
|
ModelPickerDialog(
|
||||||
|
providers =
|
||||||
|
listOf(
|
||||||
|
ModelProvider(
|
||||||
|
slug = "openai",
|
||||||
|
name = "OpenAI",
|
||||||
|
models = listOf("gpt-4"),
|
||||||
|
capabilities = mapOf("gpt-4" to ModelCapabilities(reasoning = false)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title = "Choose model",
|
||||||
|
pinnedModels = listOf(PinnedModel(providerSlug = "openai", modelName = "gpt-4")),
|
||||||
|
onPinToggle = { provider, model ->
|
||||||
|
toggledProvider = provider
|
||||||
|
toggledModel = model
|
||||||
|
},
|
||||||
|
onSelect = { provider, model ->
|
||||||
|
selectedProvider = provider
|
||||||
|
selectedModel = model
|
||||||
|
},
|
||||||
|
onDismiss = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithText("gpt-4").assertIsDisplayed()
|
||||||
|
composeTestRule.onNodeWithText("no reasoning").assertIsDisplayed()
|
||||||
|
composeTestRule.onNodeWithContentDescription("Unpin model").performClick()
|
||||||
|
composeTestRule.onNodeWithText("gpt-4").performClick()
|
||||||
|
|
||||||
|
assertEquals("openai", toggledProvider)
|
||||||
|
assertEquals("gpt-4", toggledModel)
|
||||||
|
assertEquals("openai", selectedProvider)
|
||||||
|
assertEquals("gpt-4", selectedModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -720,7 +720,7 @@ private fun ProviderCard(
|
|||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Column {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Text(
|
Text(
|
||||||
text = provider.name,
|
text = provider.name,
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
@@ -733,12 +733,36 @@ private fun ProviderCard(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCurrent) {
|
Row(
|
||||||
Text(
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
text = stringResource(R.string.model_status_current),
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
style = MaterialTheme.typography.labelMedium,
|
) {
|
||||||
color = MaterialTheme.colorScheme.primary,
|
if (isCurrent) {
|
||||||
fontWeight = FontWeight.Bold,
|
Text(
|
||||||
|
text = stringResource(R.string.model_status_current),
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Icon(
|
||||||
|
imageVector =
|
||||||
|
if (isExpanded) {
|
||||||
|
Icons.Filled.KeyboardArrowUp
|
||||||
|
} else {
|
||||||
|
Icons.Filled.KeyboardArrowDown
|
||||||
|
},
|
||||||
|
contentDescription =
|
||||||
|
stringResource(
|
||||||
|
if (isExpanded) {
|
||||||
|
R.string.content_desc_collapse_provider
|
||||||
|
} else {
|
||||||
|
R.string.content_desc_expand_provider
|
||||||
|
},
|
||||||
|
provider.name,
|
||||||
|
),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import androidx.compose.foundation.lazy.items
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||||
|
import androidx.compose.material.icons.filled.KeyboardArrowUp
|
||||||
import androidx.compose.material.icons.filled.PushPin
|
import androidx.compose.material.icons.filled.PushPin
|
||||||
import androidx.compose.material.icons.outlined.PushPin
|
import androidx.compose.material.icons.outlined.PushPin
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
@@ -25,6 +27,7 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -32,6 +35,7 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -65,6 +69,7 @@ fun ModelPickerDialog(
|
|||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
) {
|
) {
|
||||||
var pickerQuery by remember { mutableStateOf("") }
|
var pickerQuery by remember { mutableStateOf("") }
|
||||||
|
var expandedProviderSlug by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
@@ -149,6 +154,17 @@ fun ModelPickerDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(filteredProvidersWithModels) {
|
||||||
|
if (
|
||||||
|
expandedProviderSlug != null &&
|
||||||
|
filteredProvidersWithModels.none { (provider, _) ->
|
||||||
|
provider.slug == expandedProviderSlug
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
expandedProviderSlug = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SearchBar(
|
SearchBar(
|
||||||
query = pickerQuery,
|
query = pickerQuery,
|
||||||
onQueryChange = { pickerQuery = it },
|
onQueryChange = { pickerQuery = it },
|
||||||
@@ -205,45 +221,83 @@ fun ModelPickerDialog(
|
|||||||
|
|
||||||
// ── All providers / models (lazy item per model) ──
|
// ── All providers / models (lazy item per model) ──
|
||||||
filteredProvidersWithModels.forEach { (provider, models) ->
|
filteredProvidersWithModels.forEach { (provider, models) ->
|
||||||
|
val isExpanded = expandedProviderSlug == provider.slug
|
||||||
item(key = "header:${provider.slug}") {
|
item(key = "header:${provider.slug}") {
|
||||||
Row(
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
|
expandedProviderSlug =
|
||||||
|
if (isExpanded) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
provider.slug
|
||||||
|
}
|
||||||
|
}.padding(top = 12.dp, bottom = 6.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.padding(top = 12.dp, bottom = 6.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = provider.name,
|
text = provider.name,
|
||||||
style = MaterialTheme.typography.labelLarge,
|
style = MaterialTheme.typography.labelLarge,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
Text(
|
Text(
|
||||||
text = "${models.size} models",
|
text =
|
||||||
|
pluralStringResource(
|
||||||
|
R.plurals.model_provider_count,
|
||||||
|
models.size,
|
||||||
|
models.size,
|
||||||
|
),
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
|
Icon(
|
||||||
|
imageVector =
|
||||||
|
if (isExpanded) {
|
||||||
|
Icons.Filled.KeyboardArrowUp
|
||||||
|
} else {
|
||||||
|
Icons.Filled.KeyboardArrowDown
|
||||||
|
},
|
||||||
|
contentDescription =
|
||||||
|
stringResource(
|
||||||
|
if (isExpanded) {
|
||||||
|
R.string.content_desc_collapse_provider
|
||||||
|
} else {
|
||||||
|
R.string.content_desc_expand_provider
|
||||||
|
},
|
||||||
|
provider.name,
|
||||||
|
),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
items(
|
if (isExpanded) {
|
||||||
items = models,
|
items(
|
||||||
key = { model -> "${provider.slug}:$model" },
|
items = models,
|
||||||
) { model ->
|
key = { model -> "${provider.slug}:$model" },
|
||||||
val isPinned = "${provider.slug}:$model" in pinnedSet
|
) { model ->
|
||||||
val caps = provider.capabilities?.get(model)
|
val isPinned = "${provider.slug}:$model" in pinnedSet
|
||||||
ModelItemCard(
|
val caps = provider.capabilities?.get(model)
|
||||||
modelName = model,
|
ModelItemCard(
|
||||||
isPinned = isPinned,
|
modelName = model,
|
||||||
onPinToggle =
|
isPinned = isPinned,
|
||||||
if (onPinToggle != null) {
|
onPinToggle =
|
||||||
{ onPinToggle(provider.slug, model) }
|
if (onPinToggle != null) {
|
||||||
} else {
|
{ onPinToggle(provider.slug, model) }
|
||||||
null
|
} else {
|
||||||
},
|
null
|
||||||
onClick = { onSelect(provider.slug, model) },
|
},
|
||||||
canDisableReasoning = caps?.can_disable_reasoning,
|
onClick = { onSelect(provider.slug, model) },
|
||||||
supportsReasoning = caps?.reasoning,
|
canDisableReasoning = caps?.can_disable_reasoning,
|
||||||
)
|
supportsReasoning = caps?.reasoning,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -503,9 +503,19 @@
|
|||||||
<string name="model_label_available">النماذج المتاحة:</string>
|
<string name="model_label_available">النماذج المتاحة:</string>
|
||||||
<string name="model_no_models">لم يتم العثور على نماذج أو أن المزود يحتاج إلى مصادقة.</string>
|
<string name="model_no_models">لم يتم العثور على نماذج أو أن المزود يحتاج إلى مصادقة.</string>
|
||||||
<string name="content_desc_active_model">النموذج النشط</string>
|
<string name="content_desc_active_model">النموذج النشط</string>
|
||||||
|
<string name="content_desc_expand_provider">توسيع نماذج %1$s</string>
|
||||||
|
<string name="content_desc_collapse_provider">طي نماذج %1$s</string>
|
||||||
<string name="model_section_pinned">النماذج المثبتة</string>
|
<string name="model_section_pinned">النماذج المثبتة</string>
|
||||||
<string name="content_desc_unpin_model">إلغاء تثبيت النموذج</string>
|
<string name="content_desc_unpin_model">إلغاء تثبيت النموذج</string>
|
||||||
<string name="content_desc_pinned_section_toggle">تبديل ظهور قسم المثبتات</string>
|
<string name="content_desc_pinned_section_toggle">تبديل ظهور قسم المثبتات</string>
|
||||||
|
<plurals name="model_provider_count">
|
||||||
|
<item quantity="zero">لا توجد نماذج</item>
|
||||||
|
<item quantity="one">%1$d نموذج</item>
|
||||||
|
<item quantity="two">%1$d نموذجان</item>
|
||||||
|
<item quantity="few">%1$d نماذج</item>
|
||||||
|
<item quantity="many">%1$d نموذجًا</item>
|
||||||
|
<item quantity="other">%1$d نموذج</item>
|
||||||
|
</plurals>
|
||||||
<string name="history_empty_title">لا يوجد سجل</string>
|
<string name="history_empty_title">لا يوجد سجل</string>
|
||||||
<string name="history_empty_desc">ستظهر جلسات المحادثة السابقة من Hermes هنا.</string>
|
<string name="history_empty_desc">ستظهر جلسات المحادثة السابقة من Hermes هنا.</string>
|
||||||
<string name="history_untitled">بلا عنوان</string>
|
<string name="history_untitled">بلا عنوان</string>
|
||||||
|
|||||||
@@ -393,10 +393,16 @@
|
|||||||
<string name="model_label_available">사용 가능한 모델:</string>
|
<string name="model_label_available">사용 가능한 모델:</string>
|
||||||
<string name="model_no_models">보고된 모델이 없거나 인증이 필요합니다.</string>
|
<string name="model_no_models">보고된 모델이 없거나 인증이 필요합니다.</string>
|
||||||
<string name="content_desc_active_model">활성 모델</string>
|
<string name="content_desc_active_model">활성 모델</string>
|
||||||
|
<string name="content_desc_expand_provider">%1$s 모델 펼치기</string>
|
||||||
|
<string name="content_desc_collapse_provider">%1$s 모델 접기</string>
|
||||||
<string name="model_section_pinned">고정된 모델</string>
|
<string name="model_section_pinned">고정된 모델</string>
|
||||||
<string name="model_confirm_continue">계속</string>
|
<string name="model_confirm_continue">계속</string>
|
||||||
<string name="content_desc_unpin_model">모델 고정 해제</string>
|
<string name="content_desc_unpin_model">모델 고정 해제</string>
|
||||||
<string name="content_desc_pinned_section_toggle">고정 섹션 표시 전환</string>
|
<string name="content_desc_pinned_section_toggle">고정 섹션 표시 전환</string>
|
||||||
|
<plurals name="model_provider_count">
|
||||||
|
<item quantity="one">%1$d개 모델</item>
|
||||||
|
<item quantity="other">%1$d개 모델</item>
|
||||||
|
</plurals>
|
||||||
|
|
||||||
<!-- Sessions (History) Screen -->
|
<!-- Sessions (History) Screen -->
|
||||||
<string name="history_empty_title">히스토리 없음</string>
|
<string name="history_empty_title">히스토리 없음</string>
|
||||||
|
|||||||
@@ -494,9 +494,15 @@
|
|||||||
<string name="model_label_available">可用模型:</string>
|
<string name="model_label_available">可用模型:</string>
|
||||||
<string name="model_no_models">没有可用的模型,或提供商需要认证。</string>
|
<string name="model_no_models">没有可用的模型,或提供商需要认证。</string>
|
||||||
<string name="content_desc_active_model">当前模型</string>
|
<string name="content_desc_active_model">当前模型</string>
|
||||||
|
<string name="content_desc_expand_provider">展开 %1$s 模型</string>
|
||||||
|
<string name="content_desc_collapse_provider">折叠 %1$s 模型</string>
|
||||||
<string name="model_section_pinned">固定模型</string>
|
<string name="model_section_pinned">固定模型</string>
|
||||||
<string name="content_desc_unpin_model">取消固定模型</string>
|
<string name="content_desc_unpin_model">取消固定模型</string>
|
||||||
<string name="content_desc_pinned_section_toggle">切换固定区显示</string>
|
<string name="content_desc_pinned_section_toggle">切换固定区显示</string>
|
||||||
|
<plurals name="model_provider_count">
|
||||||
|
<item quantity="one">%1$d 个模型</item>
|
||||||
|
<item quantity="other">%1$d 个模型</item>
|
||||||
|
</plurals>
|
||||||
<string name="history_empty_title">暂无历史</string>
|
<string name="history_empty_title">暂无历史</string>
|
||||||
<string name="history_empty_desc">之前的 Hermes 对话会话将显示在这里。</string>
|
<string name="history_empty_desc">之前的 Hermes 对话会话将显示在这里。</string>
|
||||||
<string name="history_untitled">未命名</string>
|
<string name="history_untitled">未命名</string>
|
||||||
|
|||||||
@@ -586,9 +586,15 @@
|
|||||||
<string name="model_label_available">Available Models:</string>
|
<string name="model_label_available">Available Models:</string>
|
||||||
<string name="model_no_models">No models reported or provider needs authentication.</string>
|
<string name="model_no_models">No models reported or provider needs authentication.</string>
|
||||||
<string name="content_desc_active_model">Active Model</string>
|
<string name="content_desc_active_model">Active Model</string>
|
||||||
|
<string name="content_desc_expand_provider">Expand %1$s models</string>
|
||||||
|
<string name="content_desc_collapse_provider">Collapse %1$s models</string>
|
||||||
<string name="model_section_pinned">Pinned Models</string>
|
<string name="model_section_pinned">Pinned Models</string>
|
||||||
<string name="content_desc_unpin_model">Unpin model</string>
|
<string name="content_desc_unpin_model">Unpin model</string>
|
||||||
<string name="content_desc_pinned_section_toggle">Toggle pinned section visibility</string>
|
<string name="content_desc_pinned_section_toggle">Toggle pinned section visibility</string>
|
||||||
|
<plurals name="model_provider_count">
|
||||||
|
<item quantity="one">%1$d model</item>
|
||||||
|
<item quantity="other">%1$d models</item>
|
||||||
|
</plurals>
|
||||||
|
|
||||||
<!-- Sessions (History) Screen -->
|
<!-- Sessions (History) Screen -->
|
||||||
<string name="history_empty_title">No history</string>
|
<string name="history_empty_title">No history</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user