* feat(profiles): delete, rename, auto-describe and setup command (issue #781) * feat(profiles): use shared ModelPickerDialog for set model (issue #781) * fix(profiles): move overflow menu to card header so it can't be clipped (issue #781) --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
19c053b3fa
commit
80be168801
7 files changed
+868
-100
No files matched your search
@@ -57,6 +57,29 @@ data class UpdateProfileDescriptionRequest(
|
||||
val description: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class RenameProfileRequest(
|
||||
val new_name: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ProfileDescribeAutoRequest(
|
||||
val overwrite: Boolean = false,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ProfileDescribeAutoResponse(
|
||||
val ok: Boolean,
|
||||
val reason: String? = null,
|
||||
val description: String? = null,
|
||||
val description_auto: Boolean? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ProfileSetupCommandResponse(
|
||||
val command: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class CreateProfileRequest(
|
||||
val name: String,
|
||||
|
||||
@@ -69,11 +69,15 @@ import com.m57.hermescontrol.data.model.PairingRevokeRequest
|
||||
import com.m57.hermescontrol.data.model.PluginProvidersPutRequest
|
||||
import com.m57.hermescontrol.data.model.PluginsHubResponse
|
||||
import com.m57.hermescontrol.data.model.PortalResponse
|
||||
import com.m57.hermescontrol.data.model.ProfileDescribeAutoRequest
|
||||
import com.m57.hermescontrol.data.model.ProfileDescribeAutoResponse
|
||||
import com.m57.hermescontrol.data.model.ProfileSetupCommandResponse
|
||||
import com.m57.hermescontrol.data.model.ProfileSoulResponse
|
||||
import com.m57.hermescontrol.data.model.ProfilesResponse
|
||||
import com.m57.hermescontrol.data.model.PruneRequest
|
||||
import com.m57.hermescontrol.data.model.RawConfigResponse
|
||||
import com.m57.hermescontrol.data.model.RecentUnlock
|
||||
import com.m57.hermescontrol.data.model.RenameProfileRequest
|
||||
import com.m57.hermescontrol.data.model.SaveSkillContentRequest
|
||||
import com.m57.hermescontrol.data.model.ScanStatus
|
||||
import com.m57.hermescontrol.data.model.SessionDetailResponse
|
||||
@@ -358,6 +362,28 @@ interface HermesApiService {
|
||||
@Body body: UpdateProfileDescriptionRequest,
|
||||
): Response<Unit>
|
||||
|
||||
@PATCH("api/profiles/{name}")
|
||||
suspend fun renameProfile(
|
||||
@Path("name") name: String,
|
||||
@Body body: RenameProfileRequest,
|
||||
): Response<Unit>
|
||||
|
||||
@DELETE("api/profiles/{name}")
|
||||
suspend fun deleteProfile(
|
||||
@Path("name") name: String,
|
||||
): Response<Unit>
|
||||
|
||||
@POST("api/profiles/{name}/describe-auto")
|
||||
suspend fun describeProfileAuto(
|
||||
@Path("name") name: String,
|
||||
@Body body: ProfileDescribeAutoRequest,
|
||||
): Response<ProfileDescribeAutoResponse>
|
||||
|
||||
@GET("api/profiles/{name}/setup-command")
|
||||
suspend fun getProfileSetupCommand(
|
||||
@Path("name") name: String,
|
||||
): Response<ProfileSetupCommandResponse>
|
||||
|
||||
@GET("api/tools/toolsets")
|
||||
suspend fun getToolsets(): Response<List<Toolset>>
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -44,7 +45,10 @@ 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.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -57,6 +61,7 @@ import com.m57.hermescontrol.ui.common.HermesScaffold
|
||||
import com.m57.hermescontrol.ui.common.NavIcon
|
||||
import com.m57.hermescontrol.ui.common.SkeletonListState
|
||||
import com.m57.hermescontrol.ui.common.ToastEffect
|
||||
import com.m57.hermescontrol.ui.model.components.ModelPickerDialog
|
||||
import com.m57.hermescontrol.ui.profiles.components.ProfileBuilderView
|
||||
import com.m57.hermescontrol.ui.profiles.components.validateProfileName
|
||||
|
||||
@@ -71,8 +76,6 @@ fun ProfilesScreen(
|
||||
|
||||
var soulEditProfileName by remember { mutableStateOf<String?>(null) }
|
||||
var modelEditProfileName by remember { mutableStateOf<String?>(null) }
|
||||
var tempModelProvider by remember { mutableStateOf("") }
|
||||
var tempModelName by remember { mutableStateOf("") }
|
||||
|
||||
var cloneProfileName by remember { mutableStateOf<String?>(null) }
|
||||
var newCloneName by remember { mutableStateOf("") }
|
||||
@@ -80,6 +83,16 @@ fun ProfilesScreen(
|
||||
var descEditProfileName by remember { mutableStateOf<String?>(null) }
|
||||
var tempDescription by remember { mutableStateOf("") }
|
||||
|
||||
var renameProfileName by remember { mutableStateOf<String?>(null) }
|
||||
var newRenameName by remember { mutableStateOf("") }
|
||||
|
||||
var deleteProfileName by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
var setupCmdProfileName by remember { mutableStateOf<String?>(null) }
|
||||
var copiedSetupCmd by remember { mutableStateOf(false) }
|
||||
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
|
||||
var isBuildingProfile by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
@@ -271,6 +284,107 @@ fun ProfilesScreen(
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
)
|
||||
}
|
||||
|
||||
var showMenu by remember { mutableStateOf(false) }
|
||||
|
||||
Box {
|
||||
IconButton(onClick = { showMenu = true }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = "More options",
|
||||
)
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false },
|
||||
) {
|
||||
if (profile.is_default != true) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
stringResource(
|
||||
R.string.profiles_action_rename,
|
||||
),
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
showMenu = false
|
||||
renameProfileName = profile.name
|
||||
newRenameName = profile.name
|
||||
},
|
||||
)
|
||||
}
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
stringResource(
|
||||
R.string.profiles_action_auto_describe,
|
||||
),
|
||||
)
|
||||
},
|
||||
enabled = !state.isAutoDescribing,
|
||||
onClick = {
|
||||
showMenu = false
|
||||
viewModel.autoDescribeProfile(profile.name)
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
stringResource(
|
||||
R.string.profiles_action_edit_description,
|
||||
),
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
showMenu = false
|
||||
descEditProfileName = profile.name
|
||||
tempDescription = profile.description ?: ""
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
stringResource(R.string.profiles_action_clone),
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
showMenu = false
|
||||
cloneProfileName = profile.name
|
||||
newCloneName = ""
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
stringResource(
|
||||
R.string.profiles_action_setup_command,
|
||||
),
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
showMenu = false
|
||||
setupCmdProfileName = profile.name
|
||||
},
|
||||
)
|
||||
if (profile.is_default != true) {
|
||||
HorizontalDivider()
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
stringResource(R.string.action_delete),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
showMenu = false
|
||||
deleteProfileName = profile.name
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
@@ -358,56 +472,12 @@ fun ProfilesScreen(
|
||||
Button(
|
||||
onClick = {
|
||||
modelEditProfileName = profile.name
|
||||
tempModelProvider = profile.provider ?: ""
|
||||
tempModelName = profile.model ?: ""
|
||||
viewModel.loadModelOptions()
|
||||
},
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
) {
|
||||
Text(stringResource(R.string.profiles_action_set_model))
|
||||
}
|
||||
|
||||
var showMenu by remember { mutableStateOf(false) }
|
||||
|
||||
Box {
|
||||
IconButton(onClick = { showMenu = true }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = "More options",
|
||||
)
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false },
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
stringResource(
|
||||
R.string.profiles_action_edit_description,
|
||||
),
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
showMenu = false
|
||||
descEditProfileName = profile.name
|
||||
tempDescription = profile.description ?: ""
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
stringResource(R.string.profiles_action_clone),
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
showMenu = false
|
||||
cloneProfileName = profile.name
|
||||
newCloneName = ""
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -486,57 +556,20 @@ fun ProfilesScreen(
|
||||
}
|
||||
|
||||
if (modelEditProfileName != null) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { modelEditProfileName = null },
|
||||
title = {
|
||||
Text(
|
||||
text =
|
||||
stringResource(
|
||||
R.string.profiles_title_set_model,
|
||||
modelEditProfileName.orEmpty(),
|
||||
),
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = tempModelProvider,
|
||||
onValueChange = { tempModelProvider = it },
|
||||
label = { Text(stringResource(R.string.profiles_label_provider_input)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = tempModelName,
|
||||
onValueChange = { tempModelName = it },
|
||||
label = { Text(stringResource(R.string.profiles_label_model_input)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
val profileName = modelEditProfileName
|
||||
if (profileName != null) {
|
||||
viewModel.updateModel(profileName, tempModelProvider, tempModelName)
|
||||
}
|
||||
modelEditProfileName = null
|
||||
},
|
||||
) {
|
||||
Text(stringResource(R.string.action_ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = { modelEditProfileName = null },
|
||||
) {
|
||||
Text(stringResource(R.string.action_cancel))
|
||||
ModelPickerDialog(
|
||||
providers = state.modelProviders,
|
||||
title = stringResource(R.string.profiles_title_set_model, modelEditProfileName.orEmpty()),
|
||||
isLoading = state.isLoadingBuilderData && state.modelProviders.isEmpty(),
|
||||
pinnedModels = state.modelPickerPinned,
|
||||
onPinToggle = viewModel::togglePinModel,
|
||||
onSelect = { provider, model ->
|
||||
val profileName = modelEditProfileName
|
||||
if (profileName != null) {
|
||||
viewModel.updateModel(profileName, provider, model)
|
||||
}
|
||||
modelEditProfileName = null
|
||||
},
|
||||
onDismiss = { modelEditProfileName = null },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -633,6 +666,205 @@ fun ProfilesScreen(
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (renameProfileName != null) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { renameProfileName = null },
|
||||
title = {
|
||||
Text(
|
||||
text =
|
||||
stringResource(
|
||||
R.string.profiles_title_rename,
|
||||
renameProfileName.orEmpty(),
|
||||
),
|
||||
)
|
||||
},
|
||||
text = {
|
||||
val errorMsg = if (newRenameName.isNotEmpty()) validateProfileName(newRenameName) else null
|
||||
OutlinedTextField(
|
||||
value = newRenameName,
|
||||
onValueChange = { newRenameName = it },
|
||||
label = { Text(stringResource(R.string.profiles_label_new_name_input)) },
|
||||
isError = errorMsg != null,
|
||||
supportingText = errorMsg?.let { { Text(it) } },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
val oldName = renameProfileName
|
||||
if (oldName != null) {
|
||||
viewModel.renameProfile(oldName, newRenameName.trim())
|
||||
}
|
||||
renameProfileName = null
|
||||
},
|
||||
enabled =
|
||||
newRenameName.isNotBlank() &&
|
||||
newRenameName != renameProfileName &&
|
||||
validateProfileName(newRenameName) == null,
|
||||
) {
|
||||
Text(stringResource(R.string.profiles_action_rename))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = { renameProfileName = null },
|
||||
) {
|
||||
Text(stringResource(R.string.action_cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (deleteProfileName != null) {
|
||||
val profileToDelete = state.profiles.firstOrNull { it.name == deleteProfileName }
|
||||
AlertDialog(
|
||||
onDismissRequest = { deleteProfileName = null },
|
||||
title = {
|
||||
Text(
|
||||
text =
|
||||
stringResource(
|
||||
R.string.profiles_title_delete,
|
||||
deleteProfileName.orEmpty(),
|
||||
),
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(
|
||||
text =
|
||||
stringResource(
|
||||
R.string.profiles_delete_warning,
|
||||
deleteProfileName.orEmpty(),
|
||||
),
|
||||
)
|
||||
profileToDelete?.path?.let {
|
||||
Text(
|
||||
text = stringResource(R.string.profiles_delete_path, it),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
if (profileToDelete?.gateway_running == true) {
|
||||
Text(
|
||||
text = stringResource(R.string.profiles_delete_gateway_warning),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
val name = deleteProfileName
|
||||
if (name != null) {
|
||||
viewModel.deleteProfile(name)
|
||||
}
|
||||
deleteProfileName = null
|
||||
},
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.action_delete),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = { deleteProfileName = null },
|
||||
) {
|
||||
Text(stringResource(R.string.action_cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (setupCmdProfileName != null) {
|
||||
LaunchedEffect(setupCmdProfileName) {
|
||||
copiedSetupCmd = false
|
||||
setupCmdProfileName?.let { viewModel.fetchSetupCommand(it) }
|
||||
}
|
||||
AlertDialog(
|
||||
onDismissRequest = { setupCmdProfileName = null },
|
||||
title = {
|
||||
Text(
|
||||
text =
|
||||
stringResource(
|
||||
R.string.profiles_title_setup_command,
|
||||
setupCmdProfileName.orEmpty(),
|
||||
),
|
||||
)
|
||||
},
|
||||
text = {
|
||||
if (state.isLoadingSetupCommand) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(80.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
} else {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text(
|
||||
text = stringResource(R.string.profiles_setup_command_hint),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
.padding(12.dp),
|
||||
) {
|
||||
Text(
|
||||
text = state.setupCommand.orEmpty(),
|
||||
style =
|
||||
MaterialTheme.typography.bodyMedium.copy(
|
||||
fontFamily = FontFamily.Monospace,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
if (!state.isLoadingSetupCommand && state.setupCommand != null) {
|
||||
Button(
|
||||
onClick = {
|
||||
clipboardManager.setText(AnnotatedString(state.setupCommand.orEmpty()))
|
||||
copiedSetupCmd = true
|
||||
},
|
||||
) {
|
||||
Text(
|
||||
text =
|
||||
stringResource(
|
||||
if (copiedSetupCmd) {
|
||||
R.string.profiles_action_copied
|
||||
} else {
|
||||
R.string.profiles_action_copy
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = { setupCmdProfileName = null },
|
||||
) {
|
||||
Text(stringResource(R.string.action_cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
@@ -2,11 +2,15 @@ package com.m57.hermescontrol.ui.profiles
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.m57.hermescontrol.data.local.AuthManager
|
||||
import com.m57.hermescontrol.data.model.CloneProfileRequest
|
||||
import com.m57.hermescontrol.data.model.CreateProfileRequest
|
||||
import com.m57.hermescontrol.data.model.HubSkill
|
||||
import com.m57.hermescontrol.data.model.ModelProvider
|
||||
import com.m57.hermescontrol.data.model.PinnedModel
|
||||
import com.m57.hermescontrol.data.model.ProfileDescribeAutoRequest
|
||||
import com.m57.hermescontrol.data.model.ProfileInfo
|
||||
import com.m57.hermescontrol.data.model.RenameProfileRequest
|
||||
import com.m57.hermescontrol.data.model.SetActiveProfileRequest
|
||||
import com.m57.hermescontrol.data.model.Skill
|
||||
import com.m57.hermescontrol.data.model.UpdateProfileDescriptionRequest
|
||||
@@ -34,6 +38,12 @@ data class ProfilesUiState(
|
||||
val isLoadingSoul: Boolean = false,
|
||||
val errorMessage: String? = null,
|
||||
val toastMessage: String? = null,
|
||||
// Rename / delete / auto-describe / setup-command states
|
||||
val isAutoDescribing: Boolean = false,
|
||||
val setupCommand: String? = null,
|
||||
val isLoadingSetupCommand: Boolean = false,
|
||||
// Model picker (issue #781 — Set Model uses the shared ModelPickerDialog)
|
||||
val modelPickerPinned: List<PinnedModel> = emptyList(),
|
||||
// Profile Builder states
|
||||
val modelProviders: List<ModelProvider> = emptyList(),
|
||||
val isLoadingBuilderData: Boolean = false,
|
||||
@@ -296,6 +306,198 @@ class ProfilesViewModel :
|
||||
}
|
||||
}
|
||||
|
||||
fun renameProfile(
|
||||
oldName: String,
|
||||
newName: String,
|
||||
) {
|
||||
if (oldName == newName) return
|
||||
viewModelScope.launch {
|
||||
_uiState.update { it.copy(isLoading = true) }
|
||||
val result =
|
||||
withContext(Dispatchers.IO) {
|
||||
safeApiCall {
|
||||
ApiClient.hermesApi.renameProfile(
|
||||
oldName,
|
||||
RenameProfileRequest(newName),
|
||||
)
|
||||
}
|
||||
}
|
||||
when (result) {
|
||||
is NetworkResult.Success -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
toastMessage = "Profile '$oldName' renamed to '$newName'",
|
||||
)
|
||||
}
|
||||
loadProfiles()
|
||||
}
|
||||
|
||||
is NetworkResult.Failure -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
toastMessage = "Failed to rename profile '$oldName': ${result.error.message}",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteProfile(name: String) {
|
||||
viewModelScope.launch {
|
||||
_uiState.update { it.copy(isLoading = true) }
|
||||
val result =
|
||||
withContext(Dispatchers.IO) {
|
||||
safeApiCall { ApiClient.hermesApi.deleteProfile(name) }
|
||||
}
|
||||
when (result) {
|
||||
is NetworkResult.Success -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
toastMessage = "Profile '$name' deleted",
|
||||
)
|
||||
}
|
||||
loadProfiles()
|
||||
}
|
||||
|
||||
is NetworkResult.Failure -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
toastMessage = "Failed to delete profile '$name': ${result.error.message}",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun autoDescribeProfile(name: String) {
|
||||
_uiState.update { it.copy(isAutoDescribing = true) }
|
||||
viewModelScope.launch {
|
||||
val result =
|
||||
withContext(Dispatchers.IO) {
|
||||
safeApiCall {
|
||||
ApiClient.hermesApi.describeProfileAuto(
|
||||
name,
|
||||
ProfileDescribeAutoRequest(overwrite = true),
|
||||
)
|
||||
}
|
||||
}
|
||||
when (result) {
|
||||
is NetworkResult.Success -> {
|
||||
val body = result.data
|
||||
if (body.ok) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isAutoDescribing = false,
|
||||
toastMessage = "Auto-described profile '$name'",
|
||||
)
|
||||
}
|
||||
loadProfiles()
|
||||
} else {
|
||||
// Backend returns generation failures as ok:false + reason,
|
||||
// NOT an HTTP error — surface the reason to the user.
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isAutoDescribing = false,
|
||||
toastMessage =
|
||||
"Auto-describe failed for '$name': " +
|
||||
(body.reason ?: "unknown reason"),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is NetworkResult.Failure -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isAutoDescribing = false,
|
||||
toastMessage = "Failed to auto-describe profile '$name': ${result.error.message}",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun fetchSetupCommand(name: String) {
|
||||
_uiState.update { it.copy(isLoadingSetupCommand = true, setupCommand = null) }
|
||||
viewModelScope.launch {
|
||||
val result =
|
||||
withContext(Dispatchers.IO) {
|
||||
safeApiCall { ApiClient.hermesApi.getProfileSetupCommand(name) }
|
||||
}
|
||||
when (result) {
|
||||
is NetworkResult.Success -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isLoadingSetupCommand = false,
|
||||
setupCommand = result.data.command,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is NetworkResult.Failure -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isLoadingSetupCommand = false,
|
||||
toastMessage = "Failed to fetch setup command: ${result.error.message}",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadModelOptions() {
|
||||
_uiState.update { it.copy(isLoadingBuilderData = true, errorMessage = null) }
|
||||
viewModelScope.launch {
|
||||
val result =
|
||||
withContext(Dispatchers.IO) {
|
||||
safeApiCall { ApiClient.hermesApi.getModelOptions() }
|
||||
}
|
||||
when (result) {
|
||||
is NetworkResult.Success -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isLoadingBuilderData = false,
|
||||
modelProviders = result.data.providers,
|
||||
modelPickerPinned = AuthManager.getPinnedModels(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is NetworkResult.Failure -> {
|
||||
// Toast, not errorMessage — a picker-load failure must NOT
|
||||
// blank the profiles list with the ErrorState branch.
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isLoadingBuilderData = false,
|
||||
toastMessage = "Failed to load models: ${result.error.message}",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun togglePinModel(
|
||||
providerSlug: String,
|
||||
modelName: String,
|
||||
) {
|
||||
val target = PinnedModel(providerSlug, modelName)
|
||||
val current = AuthManager.getPinnedModels().toMutableList()
|
||||
if (target in current) {
|
||||
current.remove(target)
|
||||
} else {
|
||||
current.add(target)
|
||||
}
|
||||
AuthManager.savePinnedModels(current)
|
||||
_uiState.update { it.copy(modelPickerPinned = current) }
|
||||
}
|
||||
|
||||
fun loadBuilderData() {
|
||||
_uiState.update { it.copy(isLoadingBuilderData = true, errorMessage = null) }
|
||||
viewModelScope.launch {
|
||||
|
||||
Reference in new issue
Block a user