* feat(#441): add profile cloning and description editing * feat(#441): add profile builder wizard and API enhancements Introduce 5-step Profile Builder wizard into ProfilesScreen and add support for create/clone endpoints. * fix(#441): restore imports and add ProfileInfo constructor defaults * style: run ktlint format on codebase to fix styling errors * fix(#441): address PR review comments including provider slug bug, validation, and strings * fix(#441): remove unused McpServerConfigInput import --------- Co-authored-by: Hermes Agent <hermes@agent.local>
This commit is contained in:
1 parent
846a9380e2
commit
d181851670
37 files changed
+2299
-448
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -2,8 +2,14 @@ package com.m57.hermescontrol.ui.profiles
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
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.ProfileInfo
|
||||
import com.m57.hermescontrol.data.model.SetActiveProfileRequest
|
||||
import com.m57.hermescontrol.data.model.Skill
|
||||
import com.m57.hermescontrol.data.model.UpdateProfileDescriptionRequest
|
||||
import com.m57.hermescontrol.data.model.UpdateProfileModelRequest
|
||||
import com.m57.hermescontrol.data.model.UpdateProfileSoulRequest
|
||||
import com.m57.hermescontrol.data.remote.ApiClient
|
||||
@@ -26,9 +32,17 @@ data class ProfilesUiState(
|
||||
val isLoadingSoul: Boolean = false,
|
||||
val errorMessage: String? = null,
|
||||
val toastMessage: String? = null,
|
||||
// Profile Builder states
|
||||
val modelProviders: List<ModelProvider> = emptyList(),
|
||||
val isLoadingBuilderData: Boolean = false,
|
||||
val availableSkills: List<Skill> = emptyList(),
|
||||
val hubSearchResults: List<HubSkill> = emptyList(),
|
||||
val isSearchingHub: Boolean = false,
|
||||
)
|
||||
|
||||
class ProfilesViewModel : ViewModel(), ToastHost {
|
||||
class ProfilesViewModel :
|
||||
ViewModel(),
|
||||
ToastHost {
|
||||
private val _uiState = MutableStateFlow(ProfilesUiState())
|
||||
val uiState: StateFlow<ProfilesUiState> = _uiState.asStateFlow()
|
||||
|
||||
@@ -197,4 +211,174 @@ class ProfilesViewModel : ViewModel(), ToastHost {
|
||||
fun closeSoulDialog() {
|
||||
_uiState.update { it.copy(selectedSoulContent = null) }
|
||||
}
|
||||
|
||||
fun cloneProfile(
|
||||
sourceProfileName: String,
|
||||
newProfileName: String,
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
_uiState.update { it.copy(isLoading = true) }
|
||||
val result =
|
||||
withContext(Dispatchers.IO) {
|
||||
safeApiCall {
|
||||
ApiClient.hermesApi.cloneProfile(
|
||||
sourceProfileName,
|
||||
CloneProfileRequest(newProfileName),
|
||||
)
|
||||
}
|
||||
}
|
||||
when (result) {
|
||||
is NetworkResult.Success -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
toastMessage = "Profile '$sourceProfileName' cloned successfully to '$newProfileName'",
|
||||
)
|
||||
}
|
||||
loadProfiles()
|
||||
}
|
||||
|
||||
is NetworkResult.Failure -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
toastMessage = "Failed to clone profile '$sourceProfileName': ${result.error.message}",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateProfileDescription(
|
||||
profileName: String,
|
||||
description: String,
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
_uiState.update { it.copy(isLoading = true) }
|
||||
val result =
|
||||
withContext(Dispatchers.IO) {
|
||||
safeApiCall {
|
||||
ApiClient.hermesApi.updateProfileDescription(
|
||||
profileName,
|
||||
UpdateProfileDescriptionRequest(description),
|
||||
)
|
||||
}
|
||||
}
|
||||
when (result) {
|
||||
is NetworkResult.Success -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
toastMessage = "Profile '$profileName' description updated",
|
||||
)
|
||||
}
|
||||
loadProfiles()
|
||||
}
|
||||
|
||||
is NetworkResult.Failure -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
toastMessage = "Failed to update description for '$profileName': ${result.error.message}",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadBuilderData() {
|
||||
_uiState.update { it.copy(isLoadingBuilderData = true, errorMessage = null) }
|
||||
viewModelScope.launch {
|
||||
val modelsResult =
|
||||
withContext(Dispatchers.IO) {
|
||||
safeApiCall { ApiClient.hermesApi.getModelOptions() }
|
||||
}
|
||||
val skillsResult =
|
||||
withContext(Dispatchers.IO) {
|
||||
safeApiCall { ApiClient.hermesApi.getSkills() }
|
||||
}
|
||||
|
||||
if (modelsResult is NetworkResult.Success && skillsResult is NetworkResult.Success) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isLoadingBuilderData = false,
|
||||
modelProviders = modelsResult.data.providers,
|
||||
availableSkills = skillsResult.data,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val modelsError = (modelsResult as? NetworkResult.Failure)?.error?.message ?: "Success"
|
||||
val skillsError = (skillsResult as? NetworkResult.Failure)?.error?.message ?: "Success"
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isLoadingBuilderData = false,
|
||||
errorMessage = "Failed to load builder data: Models: $modelsError, Skills: $skillsError",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun searchHub(query: String) {
|
||||
if (query.isBlank()) return
|
||||
_uiState.update { it.copy(isSearchingHub = true) }
|
||||
viewModelScope.launch {
|
||||
val result =
|
||||
withContext(Dispatchers.IO) {
|
||||
safeApiCall { ApiClient.hermesApi.searchSkillsHub(query) }
|
||||
}
|
||||
when (result) {
|
||||
is NetworkResult.Success -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isSearchingHub = false,
|
||||
hubSearchResults = result.data,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is NetworkResult.Failure -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isSearchingHub = false,
|
||||
toastMessage = "Failed to search skills hub: ${result.error.message}",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createProfile(
|
||||
request: CreateProfileRequest,
|
||||
onSuccess: () -> Unit,
|
||||
) {
|
||||
_uiState.update { it.copy(isLoading = true) }
|
||||
viewModelScope.launch {
|
||||
val result =
|
||||
withContext(Dispatchers.IO) {
|
||||
safeApiCall { ApiClient.hermesApi.createProfile(request) }
|
||||
}
|
||||
when (result) {
|
||||
is NetworkResult.Success -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
toastMessage = "Profile ${request.name} created successfully",
|
||||
)
|
||||
}
|
||||
loadProfiles()
|
||||
onSuccess()
|
||||
}
|
||||
|
||||
is NetworkResult.Failure -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
toastMessage = "Failed to create profile: ${result.error.message}",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user