feat(ui): add CreateBotDialog to BotsScreen with avatar customizer (#981)
This commit is contained in:
8 files changed
+396
No files matched your search
@@ -20,6 +20,8 @@ object WsMethods {
|
|||||||
|
|
||||||
/** Lifetime usage snapshot (calls, token totals, compression count). */
|
/** Lifetime usage snapshot (calls, token totals, compression count). */
|
||||||
const val SESSION_USAGE = "session.usage"
|
const val SESSION_USAGE = "session.usage"
|
||||||
|
const val PROFILES_CONFIGURE = "profiles.configure"
|
||||||
|
const val PROFILES_LIST = "profiles.list"
|
||||||
|
|
||||||
// ── Interaction ───────────────────────────────────────────────────────
|
// ── Interaction ───────────────────────────────────────────────────────
|
||||||
const val PROMPT_SUBMIT = "prompt.submit"
|
const val PROMPT_SUBMIT = "prompt.submit"
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import androidx.compose.foundation.rememberScrollState
|
|||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
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.Add
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.Search
|
import androidx.compose.material.icons.filled.Search
|
||||||
import androidx.compose.material.icons.filled.Visibility
|
import androidx.compose.material.icons.filled.Visibility
|
||||||
@@ -69,8 +70,20 @@ fun BotsScreen(
|
|||||||
) {
|
) {
|
||||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
var isSearchActive by remember { mutableStateOf(false) }
|
var isSearchActive by remember { mutableStateOf(false) }
|
||||||
|
var showCreateDialog by remember { mutableStateOf(false) }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
if (showCreateDialog) {
|
||||||
|
CreateBotDialog(
|
||||||
|
onDismiss = { showCreateDialog = false },
|
||||||
|
onCreate = { name, title, description, shape, color ->
|
||||||
|
viewModel.createBot(name, title, description, shape, color) {
|
||||||
|
showCreateDialog = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
ToastEffect(
|
ToastEffect(
|
||||||
toastMessage = state.toastMessage,
|
toastMessage = state.toastMessage,
|
||||||
onClearToast = { viewModel.clearToast() },
|
onClearToast = { viewModel.clearToast() },
|
||||||
@@ -117,6 +130,15 @@ fun BotsScreen(
|
|||||||
navigationIcon = onOpenDrawer?.let { NavIcon.Menu(it) },
|
navigationIcon = onOpenDrawer?.let { NavIcon.Menu(it) },
|
||||||
actions = {
|
actions = {
|
||||||
if (!isSearchActive) {
|
if (!isSearchActive) {
|
||||||
|
IconButton(
|
||||||
|
onClick = { showCreateDialog = true },
|
||||||
|
modifier = Modifier.testTag("bots_action_create"),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Add,
|
||||||
|
contentDescription = stringResource(R.string.bots_action_create),
|
||||||
|
)
|
||||||
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { isSearchActive = true },
|
onClick = { isSearchActive = true },
|
||||||
modifier = Modifier.testTag("bots_action_search"),
|
modifier = Modifier.testTag("bots_action_search"),
|
||||||
|
|||||||
@@ -2,11 +2,16 @@ package com.m57.hermescontrol.ui.bots
|
|||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.m57.hermescontrol.data.model.BotAvatarMeta
|
||||||
|
import com.m57.hermescontrol.data.model.BotRosterMeta
|
||||||
|
import com.m57.hermescontrol.data.model.CreateProfileRequest
|
||||||
import com.m57.hermescontrol.data.model.ProfileInfo
|
import com.m57.hermescontrol.data.model.ProfileInfo
|
||||||
import com.m57.hermescontrol.data.remote.ApiClient
|
import com.m57.hermescontrol.data.remote.ApiClient
|
||||||
import com.m57.hermescontrol.data.remote.NetworkResult
|
import com.m57.hermescontrol.data.remote.NetworkResult
|
||||||
import com.m57.hermescontrol.data.remote.safeApiCall
|
import com.m57.hermescontrol.data.remote.safeApiCall
|
||||||
import com.m57.hermescontrol.data.session.ProfileSwitchCoordinator
|
import com.m57.hermescontrol.data.session.ProfileSwitchCoordinator
|
||||||
|
import com.m57.hermescontrol.data.ws.HermesWsClient
|
||||||
|
import com.m57.hermescontrol.data.ws.WsMethods
|
||||||
import com.m57.hermescontrol.ui.common.ToastHost
|
import com.m57.hermescontrol.ui.common.ToastHost
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -135,4 +140,73 @@ class BotsViewModel(
|
|||||||
override fun clearToast() {
|
override fun clearToast() {
|
||||||
_uiState.update { it.copy(toastMessage = null) }
|
_uiState.update { it.copy(toastMessage = null) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun createBot(
|
||||||
|
name: String,
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
shape: String,
|
||||||
|
color: String,
|
||||||
|
onSuccess: () -> Unit,
|
||||||
|
) {
|
||||||
|
viewModelScope.launch(ioDispatcher) {
|
||||||
|
val req =
|
||||||
|
CreateProfileRequest(
|
||||||
|
name = name,
|
||||||
|
description = description.ifBlank { null },
|
||||||
|
clone_from_default = true,
|
||||||
|
)
|
||||||
|
val result = safeApiCall { ApiClient.hermesApi.createProfile(req) }
|
||||||
|
if (result is NetworkResult.Success) {
|
||||||
|
// Configure UI metadata (avatar, custom title) via RPC
|
||||||
|
val botMeta =
|
||||||
|
BotRosterMeta(
|
||||||
|
title = title.ifBlank { null },
|
||||||
|
description = description.ifBlank { null },
|
||||||
|
avatar =
|
||||||
|
BotAvatarMeta(
|
||||||
|
shape = shape,
|
||||||
|
color = color,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
wsClientConfigureBot(name, botMeta)
|
||||||
|
loadBots()
|
||||||
|
onSuccess()
|
||||||
|
} else {
|
||||||
|
val err =
|
||||||
|
(result as? NetworkResult.Failure)?.error?.message
|
||||||
|
?: "Failed to create bot"
|
||||||
|
_uiState.update { it.copy(errorMessage = err) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun wsClientConfigureBot(
|
||||||
|
name: String,
|
||||||
|
meta: BotRosterMeta,
|
||||||
|
) {
|
||||||
|
val metaMap =
|
||||||
|
buildMap<String, Any?> {
|
||||||
|
put("title", meta.title)
|
||||||
|
put("description", meta.description)
|
||||||
|
meta.avatar?.let { av ->
|
||||||
|
put(
|
||||||
|
"avatar",
|
||||||
|
buildMap<String, Any?> {
|
||||||
|
av.shape?.let { put("shape", it) }
|
||||||
|
av.color?.let { put("color", it) }
|
||||||
|
av.icon?.let { put("icon", it) }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HermesWsClient.send(
|
||||||
|
WsMethods.PROFILES_CONFIGURE,
|
||||||
|
mapOf(
|
||||||
|
"name" to name,
|
||||||
|
"ui_meta" to mapOf("hermes-bots" to metaMap),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
package com.m57.hermescontrol.ui.bots
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.FilterChip
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
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.graphics.Color
|
||||||
|
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.BotAvatarMeta
|
||||||
|
import com.m57.hermescontrol.theme.parseHexColor
|
||||||
|
import com.m57.hermescontrol.ui.common.BotAvatar
|
||||||
|
|
||||||
|
private val AVAILABLE_SHAPES = listOf("circle", "square", "rounded", "hexagon")
|
||||||
|
private val PALETTE_COLORS =
|
||||||
|
listOf(
|
||||||
|
"#4F46E5", // Indigo
|
||||||
|
"#2563EB", // Blue
|
||||||
|
"#0D9488", // Teal
|
||||||
|
"#16A34A", // Green
|
||||||
|
"#D97706", // Amber
|
||||||
|
"#EA580C", // Orange
|
||||||
|
"#DC2626", // Red
|
||||||
|
"#DB2777", // Pink
|
||||||
|
"#9333EA", // Purple
|
||||||
|
"#4B5563", // Gray
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CreateBotDialog(
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onCreate: (name: String, title: String, description: String, shape: String, color: String) -> Unit,
|
||||||
|
) {
|
||||||
|
var name by remember { mutableStateOf("") }
|
||||||
|
var title by remember { mutableStateOf("") }
|
||||||
|
var description by remember { mutableStateOf("") }
|
||||||
|
var selectedShape by remember { mutableStateOf("circle") }
|
||||||
|
var selectedColor by remember { mutableStateOf(PALETTE_COLORS[0]) }
|
||||||
|
|
||||||
|
val isValid = name.isNotBlank() && name.trim().matches(Regex("^[a-zA-Z0-9_-]+$"))
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.bots_create_title),
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Column(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
) {
|
||||||
|
// Live Avatar Preview Header
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 8.dp),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
BotAvatar(
|
||||||
|
name = name.ifBlank { "bot" },
|
||||||
|
avatar =
|
||||||
|
BotAvatarMeta(
|
||||||
|
shape = selectedShape,
|
||||||
|
color = selectedColor,
|
||||||
|
),
|
||||||
|
size = 64.dp,
|
||||||
|
showPresence = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = name,
|
||||||
|
onValueChange = { name = it.filter { c -> c.isLetterOrDigit() || c == '_' || c == '-' } },
|
||||||
|
label = { Text(stringResource(R.string.bots_create_name_label)) },
|
||||||
|
placeholder = { Text(stringResource(R.string.bots_create_name_placeholder)) },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = title,
|
||||||
|
onValueChange = { title = it },
|
||||||
|
label = { Text(stringResource(R.string.bots_create_title_label)) },
|
||||||
|
placeholder = { Text(stringResource(R.string.bots_create_title_placeholder)) },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = description,
|
||||||
|
onValueChange = { description = it },
|
||||||
|
label = { Text(stringResource(R.string.bots_create_desc_label)) },
|
||||||
|
placeholder = { Text(stringResource(R.string.bots_create_desc_placeholder)) },
|
||||||
|
maxLines = 3,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.bots_create_avatar_shape),
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
|
) {
|
||||||
|
AVAILABLE_SHAPES.forEach { shape ->
|
||||||
|
FilterChip(
|
||||||
|
selected = selectedShape == shape,
|
||||||
|
onClick = { selectedShape = shape },
|
||||||
|
label = { Text(shape.replaceFirstChar { it.uppercase() }) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.bots_create_avatar_color),
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
PALETTE_COLORS.take(5).forEach { hex ->
|
||||||
|
val color = parseHexColor(hex, Color.Gray)
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.size(32.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(color)
|
||||||
|
.clickable { selectedColor = hex }
|
||||||
|
.then(
|
||||||
|
if (selectedColor == hex) {
|
||||||
|
Modifier.border(2.dp, MaterialTheme.colorScheme.onSurface, CircleShape)
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
PALETTE_COLORS.drop(5).forEach { hex ->
|
||||||
|
val color = parseHexColor(hex, Color.Gray)
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.size(32.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(color)
|
||||||
|
.clickable { selectedColor = hex }
|
||||||
|
.then(
|
||||||
|
if (selectedColor == hex) {
|
||||||
|
Modifier.border(2.dp, MaterialTheme.colorScheme.onSurface, CircleShape)
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
if (isValid) {
|
||||||
|
onCreate(name.trim(), title.trim(), description.trim(), selectedShape, selectedColor)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = isValid,
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.bots_create_button))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text(stringResource(R.string.action_cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -71,6 +71,17 @@
|
|||||||
<string name="bots_hidden_badge">숨김</string>
|
<string name="bots_hidden_badge">숨김</string>
|
||||||
<string name="bots_show_hidden">숨겨진 봇 표시</string>
|
<string name="bots_show_hidden">숨겨진 봇 표시</string>
|
||||||
<string name="bots_hide_hidden">숨겨진 봇 숨기기</string>
|
<string name="bots_hide_hidden">숨겨진 봇 숨기기</string>
|
||||||
|
<string name="bots_action_create">봇 생성</string>
|
||||||
|
<string name="bots_create_title">새 봇 생성</string>
|
||||||
|
<string name="bots_create_name_label">식별자 / 슬러그</string>
|
||||||
|
<string name="bots_create_name_placeholder">예: scout, reviewer</string>
|
||||||
|
<string name="bots_create_title_label">표시 이름</string>
|
||||||
|
<string name="bots_create_title_placeholder">예: 스카우트 에이전트</string>
|
||||||
|
<string name="bots_create_desc_label">설명</string>
|
||||||
|
<string name="bots_create_desc_placeholder">이 에이전트는 어떤 작업을 수행하나요?</string>
|
||||||
|
<string name="bots_create_avatar_shape">아바타 모양</string>
|
||||||
|
<string name="bots_create_avatar_color">강조 색상</string>
|
||||||
|
<string name="bots_create_button">생성</string>
|
||||||
<string name="screen_webhooks">웹훅</string>
|
<string name="screen_webhooks">웹훅</string>
|
||||||
<string name="screen_gateway">게이트웨이</string>
|
<string name="screen_gateway">게이트웨이</string>
|
||||||
<string name="screen_toolsets">툴셋</string>
|
<string name="screen_toolsets">툴셋</string>
|
||||||
|
|||||||
@@ -75,6 +75,17 @@
|
|||||||
<string name="bots_hidden_badge">已隐藏</string>
|
<string name="bots_hidden_badge">已隐藏</string>
|
||||||
<string name="bots_show_hidden">显示隐藏的智能体</string>
|
<string name="bots_show_hidden">显示隐藏的智能体</string>
|
||||||
<string name="bots_hide_hidden">隐藏不显示的智能体</string>
|
<string name="bots_hide_hidden">隐藏不显示的智能体</string>
|
||||||
|
<string name="bots_action_create">创建 Bot</string>
|
||||||
|
<string name="bots_create_title">新建 Bot</string>
|
||||||
|
<string name="bots_create_name_label">标识 / 别名</string>
|
||||||
|
<string name="bots_create_name_placeholder">例如 scout, reviewer</string>
|
||||||
|
<string name="bots_create_title_label">显示名称</string>
|
||||||
|
<string name="bots_create_title_placeholder">例如 Scout 助手</string>
|
||||||
|
<string name="bots_create_desc_label">描述</string>
|
||||||
|
<string name="bots_create_desc_placeholder">此智能体负责什么?</string>
|
||||||
|
<string name="bots_create_avatar_shape">头像形状</string>
|
||||||
|
<string name="bots_create_avatar_color">主题色</string>
|
||||||
|
<string name="bots_create_button">创建</string>
|
||||||
<string name="screen_webhooks">Webhook</string>
|
<string name="screen_webhooks">Webhook</string>
|
||||||
<string name="screen_gateway">网关</string>
|
<string name="screen_gateway">网关</string>
|
||||||
<string name="screen_toolsets">工具集</string>
|
<string name="screen_toolsets">工具集</string>
|
||||||
|
|||||||
@@ -88,6 +88,17 @@
|
|||||||
<string name="bots_hidden_badge">Hidden</string>
|
<string name="bots_hidden_badge">Hidden</string>
|
||||||
<string name="bots_show_hidden">Show hidden bots</string>
|
<string name="bots_show_hidden">Show hidden bots</string>
|
||||||
<string name="bots_hide_hidden">Hide hidden bots</string>
|
<string name="bots_hide_hidden">Hide hidden bots</string>
|
||||||
|
<string name="bots_action_create">Create Bot</string>
|
||||||
|
<string name="bots_create_title">New Bot</string>
|
||||||
|
<string name="bots_create_name_label">Handle / Slug</string>
|
||||||
|
<string name="bots_create_name_placeholder">e.g. scout, reviewer</string>
|
||||||
|
<string name="bots_create_title_label">Display Name</string>
|
||||||
|
<string name="bots_create_title_placeholder">e.g. Scout Agent</string>
|
||||||
|
<string name="bots_create_desc_label">Description</string>
|
||||||
|
<string name="bots_create_desc_placeholder">What does this agent do?</string>
|
||||||
|
<string name="bots_create_avatar_shape">Avatar Shape</string>
|
||||||
|
<string name="bots_create_avatar_color">Accent Color</string>
|
||||||
|
<string name="bots_create_button">Create</string>
|
||||||
<string name="screen_webhooks">Webhooks</string>
|
<string name="screen_webhooks">Webhooks</string>
|
||||||
<string name="screen_gateway">Gateway</string>
|
<string name="screen_gateway">Gateway</string>
|
||||||
<string name="screen_toolsets">Toolsets</string>
|
<string name="screen_toolsets">Toolsets</string>
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ import com.m57.hermescontrol.data.model.ProfileWorkerSummary
|
|||||||
import com.m57.hermescontrol.data.model.ProfilesResponse
|
import com.m57.hermescontrol.data.model.ProfilesResponse
|
||||||
import com.m57.hermescontrol.data.remote.ApiClient
|
import com.m57.hermescontrol.data.remote.ApiClient
|
||||||
import com.m57.hermescontrol.data.remote.HermesApiService
|
import com.m57.hermescontrol.data.remote.HermesApiService
|
||||||
|
import com.m57.hermescontrol.data.ws.HermesWsClient
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.coVerify
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.mockkObject
|
import io.mockk.mockkObject
|
||||||
@@ -43,11 +45,14 @@ class BotsViewModelTest {
|
|||||||
mockkObject(ApiClient)
|
mockkObject(ApiClient)
|
||||||
mockApi = mockk(relaxed = true)
|
mockApi = mockk(relaxed = true)
|
||||||
every { ApiClient.hermesApi } returns mockApi
|
every { ApiClient.hermesApi } returns mockApi
|
||||||
|
mockkObject(HermesWsClient)
|
||||||
|
every { HermesWsClient.send(any(), any(), any()) } returns "req-1"
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
fun tearDown() {
|
fun tearDown() {
|
||||||
unmockkObject(ApiClient)
|
unmockkObject(ApiClient)
|
||||||
|
unmockkObject(HermesWsClient)
|
||||||
Dispatchers.resetMain()
|
Dispatchers.resetMain()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,4 +141,27 @@ class BotsViewModelTest {
|
|||||||
val searchResults = viewModel.uiState.value.displayProfiles.map { it.name }
|
val searchResults = viewModel.uiState.value.displayProfiles.map { it.name }
|
||||||
assertEquals(listOf("scout"), searchResults)
|
assertEquals(listOf("scout"), searchResults)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCreateBot_callsApiAndConfiguresMeta() =
|
||||||
|
runTest {
|
||||||
|
coEvery { mockApi.createProfile(any()) } returns Response.success(Unit)
|
||||||
|
coEvery { mockApi.getProfiles() } returns Response.success(ProfilesResponse(emptyList()))
|
||||||
|
coEvery { mockApi.getActiveProfile() } returns Response.success(ActiveProfileResponse(active = "default"))
|
||||||
|
|
||||||
|
val viewModel = BotsViewModel(ioDispatcher = testDispatcher, autoLoad = false)
|
||||||
|
var successCalled = false
|
||||||
|
viewModel.createBot(
|
||||||
|
name = "researcher",
|
||||||
|
title = "Research Bot",
|
||||||
|
description = "Deep research agent",
|
||||||
|
shape = "hexagon",
|
||||||
|
color = "#4F46E5",
|
||||||
|
onSuccess = { successCalled = true },
|
||||||
|
)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
assertTrue(successCalled)
|
||||||
|
coVerify(exactly = 1) { mockApi.createProfile(match { it.name == "researcher" }) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user