Refactor: Implement V2 of Create Agent UI and logic

- Introduced `CreateAgentV2Screen` and `CreateAgentV2ViewModel` for a new agent creation experience.
- Implemented AI-powered agent info generation based on user input, including title and description.
- Added a "manual mode" for users who prefer to input agent details directly.
- Enhanced UI with gradient borders, loading animations, and improved layout.
- Integrated avatar selection and cropping using `AgentImageCropScreen`.
- Refactored `AddAgentViewModel` to support state persistence across page navigation and to store generated input text.
- Updated API client to include a longer timeout for agent info generation requests.
- Added new drawable resources for UI elements.
- Switched `Const.BASE_SERVER` to use the release URL for debug builds.
- Replaced the old `AddAgentScreen` with the new `CreateAgentV2Screen` in navigation.
This commit is contained in:
2025-09-15 12:03:39 +08:00
parent eca85c8377
commit cf25540417
10 changed files with 1000 additions and 6 deletions

View File

@@ -4,7 +4,7 @@ object ConstVars {
// api 地址 - 根据构建类型自动选择
// Debug: http://192.168.0.201:8088
// Release: https://rider-pro.aiosman.com/beta_api
val BASE_SERVER = if (BuildConfig.DEBUG) {
val BASE_SERVER = if (!BuildConfig.DEBUG) {
"http://47.109.137.67:6363" // Debug环境
} else {
"https://rider-pro.aiosman.com/beta_api" // Release环境

View File

@@ -13,9 +13,11 @@ import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.Date
import java.util.Locale
import java.util.concurrent.TimeUnit
fun getSafeOkHttpClient(
authInterceptor: AuthInterceptor? = null
authInterceptor: AuthInterceptor? = null,
timeoutSeconds: Long = 30
): OkHttpClient {
return OkHttpClient.Builder()
.apply {
@@ -23,6 +25,9 @@ fun getSafeOkHttpClient(
addInterceptor(it)
}
}
.connectTimeout(timeoutSeconds, TimeUnit.SECONDS)
.readTimeout(timeoutSeconds, TimeUnit.SECONDS)
.writeTimeout(timeoutSeconds, TimeUnit.SECONDS)
.build()
}
@@ -56,7 +61,7 @@ class AuthInterceptor() : Interceptor {
val client = Retrofit.Builder()
.baseUrl(ApiClient.RETROFIT_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(getSafeOkHttpClient())
.client(getSafeOkHttpClient(timeoutSeconds = 30))
.build()
.create(RaveNowAPI::class.java)
@@ -75,7 +80,10 @@ object ApiClient {
val RETROFIT_URL = "${BASE_API_URL}/"
const val TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"
private val okHttpClient: OkHttpClient by lazy {
getSafeOkHttpClient(authInterceptor = AuthInterceptor())
getSafeOkHttpClient(authInterceptor = AuthInterceptor(), timeoutSeconds = 30)
}
private val longTimeoutOkHttpClient: OkHttpClient by lazy {
getSafeOkHttpClient(authInterceptor = AuthInterceptor(), timeoutSeconds = 120)
}
private val retrofit: Retrofit by lazy {
Retrofit.Builder()
@@ -84,9 +92,19 @@ object ApiClient {
.addConverterFactory(GsonConverterFactory.create())
.build()
}
private val longTimeoutRetrofit: Retrofit by lazy {
Retrofit.Builder()
.baseUrl(RETROFIT_URL)
.client(longTimeoutOkHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
val api: RaveNowAPI by lazy {
retrofit.create(RaveNowAPI::class.java)
}
val longTimeoutApi: RaveNowAPI by lazy {
longTimeoutRetrofit.create(RaveNowAPI::class.java)
}
fun formatTime(date: Date): String {
val dateFormat = SimpleDateFormat(TIME_FORMAT, Locale.getDefault())

View File

@@ -42,6 +42,20 @@ data class AgentMomentRequestBody(
val sessionId: String
)
data class GenerateAgentInfoRequestBody(
@SerializedName("descriptionText")
val descriptionText: String
)
data class GenerateAgentInfoResponseBody(
@SerializedName("title")
val title: String,
@SerializedName("description")
val description: String,
@SerializedName("content")
val content: String
)
data class SingleChatRequestBody(
@SerializedName("agentOpenId")
val agentOpenId: String? = null,
@@ -605,7 +619,8 @@ interface RaveNowAPI {
suspend fun joinRoom(@Body body: JoinGroupChatRequestBody,
): Response<DataContainer<Room>>
@POST("outside/generate/agent-info")
suspend fun generateAgentInfo(@Body body: GenerateAgentInfoRequestBody): Response<DataContainer<GenerateAgentInfoResponseBody>>
}

View File

@@ -39,6 +39,7 @@ import com.aiosman.ravenow.ui.account.RemoveAccountScreen
import com.aiosman.ravenow.ui.account.ResetPasswordScreen
import com.aiosman.ravenow.ui.agent.AddAgentScreen
import com.aiosman.ravenow.ui.agent.AgentImageCropScreen
import com.aiosman.ravenow.ui.agent.CreateAgentV2Screen
import com.aiosman.ravenow.ui.group.CreateGroupChatScreen
import com.aiosman.ravenow.ui.chat.ChatAiScreen
import com.aiosman.ravenow.ui.chat.ChatScreen
@@ -544,7 +545,7 @@ fun NavigationController(
composable(
route = NavigationRoute.AddAgent.route,
) {
AddAgentScreen()
CreateAgentV2Screen()
}
composable(

View File

@@ -24,6 +24,10 @@ object AddAgentViewModel : ViewModel() {
var croppedBitmap by mutableStateOf<Bitmap?>(null)
var isUpdating by mutableStateOf(false)
var isSelectingAvatar by mutableStateOf(false) // 标记是否正在选择头像
var hasExitedPage by mutableStateOf(false) // 标记是否已经完全退出页面
// 保存AI生成的输入文本避免页面重建时丢失
var generateInputText by mutableStateOf("")
suspend fun updateAgentAvatar(context: Context) {
croppedBitmap?.let {
@@ -84,5 +88,7 @@ object AddAgentViewModel : ViewModel() {
croppedBitmap = null
isUpdating = false
isSelectingAvatar = false
hasExitedPage = false
generateInputText = ""
}
}

View File

@@ -0,0 +1,648 @@
package com.aiosman.ravenow.ui.agent
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.animation.core.*
import androidx.compose.ui.geometry.Offset
import kotlin.math.cos
import kotlin.math.sin
import androidx.compose.runtime.LaunchedEffect
import kotlinx.coroutines.delay
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.composables.ActionButton
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
@Composable
fun LoadingDots(
modifier: Modifier = Modifier,
dotColor: Color = Color.Gray
) {
val infiniteTransition = rememberInfiniteTransition(label = "loading_dots")
val animationValues = (0..2).map { index ->
infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 600,
easing = EaseInOut,
delayMillis = index * 200
),
repeatMode = RepeatMode.Reverse
),
label = "dot_$index"
)
}
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
animationValues.forEach { animValue ->
Box(
modifier = Modifier
.size(6.dp)
.offset(y = (-8 * animValue.value).dp)
.background(
color = dotColor.copy(alpha = 0.5f + 0.5f * animValue.value),
shape = CircleShape
)
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CreateAgentV2Screen(
viewModel: CreateAgentV2ViewModel = remember { CreateAgentV2ViewModel() }
) {
// 页面进入时的状态管理
LaunchedEffect(Unit) {
// 总是先同步状态
viewModel.syncStateOnResume()
}
// 页面退出时的处理
DisposableEffect(Unit) {
onDispose {
// 页面退出时,标记为已退出(除非是在选择头像)
if (!viewModel.isSelectingAvatar) {
viewModel.markPageExited()
}
}
}
val appColors = LocalAppTheme.current
val navController = LocalNavController.current
val context = LocalContext.current
// 获取当前用户名,如果没有则使用默认值
val userName = AppState.profile?.nickName ?: "用户"
// 渐变边框旋转动画
val infiniteTransition = rememberInfiniteTransition(label = "gradient_rotation")
val rotationAngle by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 16000,
easing = LinearEasing
),
repeatMode = RepeatMode.Restart
),
label = "rotation_angle"
)
Column(
modifier = Modifier
.fillMaxSize()
.background(appColors.background)
) {
// 状态栏占位
StatusBarSpacer()
// 顶部标题栏
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
// 返回按钮
Image(
painter = painterResource(id = R.drawable.rider_pro_back_icon),
contentDescription = "返回",
colorFilter = ColorFilter.tint(appColors.text),
modifier = Modifier
.size(24.dp)
.noRippleClickable {
navController.navigateUp()
}
)
Spacer(modifier = Modifier.width(12.dp))
// 标题 - 左对齐
Text(
text = "创建AI",
fontSize = 18.sp,
fontWeight = FontWeight.W700,
color = appColors.text
)
}
// 主要内容区域 - 可滚动
Column(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.verticalScroll(rememberScrollState())
.padding(horizontal = 24.dp),
horizontalAlignment = Alignment.Start
) {
Spacer(modifier = Modifier.height(40.dp))
// AI头像图标
Box(
modifier = Modifier
.size(48.dp)
.background(
color = appColors.inputBackground,
shape = CircleShape
),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(R.drawable.ic_create_head_logo),
contentDescription = "AI头像",
modifier = Modifier.size(48.dp),
)
}
Spacer(modifier = Modifier.height(32.dp))
// 问候语
Text(
text = "$userName 你好呀!今天想创建什么?",
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = appColors.text,
textAlign = TextAlign.Start
)
Spacer(modifier = Modifier.height(16.dp))
// 描述性文字
Text(
text = "只需要一句话你的专属AI在这里诞生",
fontSize = 14.sp,
color = appColors.secondaryText,
textAlign = TextAlign.Start
)
Spacer(modifier = Modifier.height(40.dp))
// 根据模式显示不同的UI
if (!viewModel.isManualMode) {
// AI生成模式 - 渐变边框输入框
Box(
modifier = Modifier
.fillMaxWidth()
.shadow(
elevation = 8.dp,
shape = RoundedCornerShape(16.dp),
ambientColor = Color(0xFF6246ff).copy(alpha = 0.4f),
spotColor = Color(0xFFd80264).copy(alpha = 0.4f)
)
) {
// 渐变边框
Box(
modifier = Modifier
.fillMaxWidth()
.background(
brush = Brush.linearGradient(
colors = listOf(
Color(0xFF6246ff), // 紫色
Color(0xFFd80264), // 红色
Color(0xFF6246ff), // 紫色
Color(0xFFd80264) // 红色
),
start = Offset(
x = cos(Math.toRadians(rotationAngle.toDouble())).toFloat() * 1000f,
y = sin(Math.toRadians(rotationAngle.toDouble())).toFloat() * 1000f
),
end = Offset(
x = cos(Math.toRadians(rotationAngle.toDouble() + 180)).toFloat() * 1000f,
y = sin(Math.toRadians(rotationAngle.toDouble() + 180)).toFloat() * 1000f
)
),
shape = RoundedCornerShape(16.dp)
)
.padding(1.5.dp) // 边框宽度
) {
// 内部输入框
Box(
modifier = Modifier
.fillMaxWidth()
.background(
color = appColors.background,
shape = RoundedCornerShape(15.dp)
)
.padding(8.dp)
) {
Column {
TextField(
value = viewModel.inputText,
onValueChange = {
if (!viewModel.isGenerating) {
viewModel.updateInputText(it)
}
},
placeholder = {
Text(
text = "一个会写诗的AI一个会懂你笑点的AI",
color = appColors.inputHint,
fontSize = 14.sp
)
},
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
disabledContainerColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
disabledIndicatorColor = Color.Transparent,
focusedTextColor = appColors.text,
unfocusedTextColor = appColors.text,
disabledTextColor = appColors.inputHint,
cursorColor = if (viewModel.isGenerating) Color.Transparent else appColors.main,
focusedPlaceholderColor = appColors.inputHint,
unfocusedPlaceholderColor = appColors.inputHint,
disabledPlaceholderColor = appColors.inputHint.copy(alpha = 0.5f)
),
enabled = !viewModel.isGenerating,
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 100.dp)
)
// AI美化按钮
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
TextButton(
onClick = {
if (!viewModel.isGenerating) {
viewModel.generateAgentInfo()
}
},
enabled = viewModel.canGenerate() && !viewModel.isGenerating,
colors = ButtonDefaults.textButtonColors(
contentColor = if (viewModel.isGenerating) appColors.inputHint else Color(0xFF7c46ed),
disabledContentColor = appColors.inputHint
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Image(
painter = painterResource(R.drawable.ic_create_agent_generate),
contentDescription = "AI美化图标",
modifier = Modifier.size(16.dp),
colorFilter = ColorFilter.tint(Color(0xFF7c46ed))
)
Text(
text = if (viewModel.isGenerating) "生成中..." else "AI美化",
fontSize = 12.sp,
fontWeight = FontWeight.Medium
)
}
}
}
}
}
}
}
Spacer(modifier = Modifier.height(16.dp))
// AI生成中的loading状态
if (viewModel.isGenerating) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
LoadingDots(
dotColor = appColors.main
)
Text(
text = "正在为你构思",
fontSize = 14.sp,
color = appColors.secondaryText,
fontWeight = FontWeight.Medium
)
}
}
Spacer(modifier = Modifier.height(16.dp))
}
// 手动创造AI按钮 - 只在非生成状态下显示
if (!viewModel.isGenerating) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Start
) {
OutlinedButton(
onClick = {
if (!viewModel.isGenerating) {
viewModel.enableManualMode()
}
},
enabled = !viewModel.isGenerating,
modifier = Modifier.height(40.dp),
shape = RoundedCornerShape(10.dp),
border = BorderStroke(1.dp, appColors.inputHint.copy(alpha = 0.3f)),
colors = ButtonDefaults.outlinedButtonColors(
contentColor = appColors.secondaryText,
disabledContentColor = appColors.inputHint
),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
Icon(
Icons.Default.Edit,
contentDescription = "编辑图标",
modifier = Modifier.size(16.dp),
tint = appColors.secondaryText
)
Text(
text = "手动创造AI",
fontSize = 13.sp,
fontWeight = FontWeight.Medium
)
}
}
}
}
} else {
// 手动模式 - "一句话创造AI"按钮
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Start
) {
Box(
modifier = Modifier
.shadow(
elevation = 6.dp,
shape = RoundedCornerShape(12.dp),
ambientColor = Color(0xFF6246ff).copy(alpha = 0.3f),
spotColor = Color(0xFFd80264).copy(alpha = 0.3f)
)
) {
// 渐变边框
Box(
modifier = Modifier
.background(
brush = Brush.linearGradient(
colors = listOf(
Color(0xFF6246ff), // 紫色
Color(0xFFd80264), // 红色
Color(0xFF6246ff), // 紫色
Color(0xFFd80264) // 红色
),
start = Offset(
x = cos(Math.toRadians(rotationAngle.toDouble())).toFloat() * 1000f,
y = sin(Math.toRadians(rotationAngle.toDouble())).toFloat() * 1000f
),
end = Offset(
x = cos(Math.toRadians(rotationAngle.toDouble() + 180)).toFloat() * 1000f,
y = sin(Math.toRadians(rotationAngle.toDouble() + 180)).toFloat() * 1000f
)
),
shape = RoundedCornerShape(12.dp)
)
.padding(1.dp) // 边框宽度
) {
// 内部按钮
Box(
modifier = Modifier
.background(
color = appColors.background,
shape = RoundedCornerShape(11.dp)
)
.noRippleClickable {
viewModel.disableManualMode()
}
.padding(horizontal = 12.dp, vertical = 8.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
Icon(
Icons.Default.Edit,
contentDescription = "编辑图标",
modifier = Modifier.size(16.dp),
tint = appColors.secondaryText
)
Text(
text = "一句话创造AI",
fontSize = 13.sp,
fontWeight = FontWeight.Medium,
color = appColors.secondaryText
)
}
}
}
}
}
}
// 生成结果显示区域
if (viewModel.hasGeneratedResult()) {
Spacer(modifier = Modifier.height(32.dp))
// 头像选择组件
Box(
modifier = Modifier
.size(72.dp)
.noRippleClickable {
viewModel.setSelectingAvatar(true)
navController.navigate(NavigationRoute.AgentImageCrop.route)
},
contentAlignment = Alignment.Center
) {
if (viewModel.croppedBitmap != null) {
// 有头像时显示头像
CustomAsyncImage(
context,
viewModel.croppedBitmap,
modifier = Modifier
.size(72.dp)
.clip(CircleShape),
contentDescription = "AI头像",
contentScale = ContentScale.Crop,
placeholderRes = R.mipmap.rider_pro_agent_avatar,
showShimmer = false
)
} else {
// 没有头像时显示渐变背景和编辑图标
Box(
modifier = Modifier
.size(72.dp)
.clip(CircleShape)
.background(
brush = Brush.verticalGradient(
0f to Color(0xFF7c45ed),
0.24f to Color(0xFF7c68ef),
1f to Color(0xFF7bd8f8)
)
),
contentAlignment = Alignment.Center
) {
Icon(
Icons.Default.Edit,
contentDescription = "选择头像",
tint = Color.White,
modifier = Modifier.size(24.dp)
)
}
}
}
Spacer(modifier = Modifier.height(32.dp))
// 标题输入框
Column(
modifier = Modifier.fillMaxWidth()
) {
Text(
text = "名称",
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
color = appColors.text,
modifier = Modifier.padding(bottom = 8.dp)
)
TextField(
value = viewModel.agentTitle,
onValueChange = { viewModel.updateAgentTitle(it) },
colors = TextFieldDefaults.colors(
focusedContainerColor = appColors.inputBackground,
unfocusedContainerColor = appColors.inputBackground,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
focusedTextColor = appColors.text,
unfocusedTextColor = appColors.text
),
shape = RoundedCornerShape(12.dp),
modifier = Modifier.fillMaxWidth()
)
}
Spacer(modifier = Modifier.height(16.dp))
// 描述输入框
Column(
modifier = Modifier.fillMaxWidth()
) {
Text(
text = "描述",
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
color = appColors.text,
modifier = Modifier.padding(bottom = 8.dp)
)
TextField(
value = viewModel.agentDescription,
onValueChange = { viewModel.updateAgentDescription(it) },
colors = TextFieldDefaults.colors(
focusedContainerColor = appColors.inputBackground,
unfocusedContainerColor = appColors.inputBackground,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
focusedTextColor = appColors.text,
unfocusedTextColor = appColors.text
),
shape = RoundedCornerShape(12.dp),
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 120.dp)
)
}
Spacer(modifier = Modifier.height(32.dp))
// 错误信息显示
viewModel.errorMessage?.let { error ->
Text(
text = error,
color = Color.Red,
modifier = Modifier.padding(horizontal = 16.dp),
fontSize = 14.sp
)
Spacer(modifier = Modifier.height(16.dp))
}
// 创建AI按钮
ActionButton(
modifier = Modifier
.fillMaxWidth()
.background(
brush = Brush.linearGradient(
colors = listOf(
Color(0xFF7bd8f8),
Color(0xFF7c68ef),
Color(0xFF7c45ed)
)
),
shape = RoundedCornerShape(24.dp)
),
color = Color.White,
backgroundColor = Color.Transparent,
text = "好的,就它了",
isLoading = viewModel.isCreating,
loadingText = "创建中...",
enabled = viewModel.canCreate()
) {
viewModel.createAgent(context) {
navController.popBackStack()
}
}
}
Spacer(modifier = Modifier.height(32.dp))
}
}
}

View File

@@ -0,0 +1,238 @@
package com.aiosman.ravenow.ui.agent
import android.content.Context
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.data.api.GenerateAgentInfoRequestBody
import kotlinx.coroutines.launch
class CreateAgentV2ViewModel : ViewModel() {
// UI状态
var inputText by mutableStateOf("")
private set
var agentTitle by mutableStateOf("")
private set
var agentDescription by mutableStateOf("")
private set
var isGenerating by mutableStateOf(false)
private set
var errorMessage by mutableStateOf<String?>(null)
private set
var isCreating by mutableStateOf(false)
private set
var isManualMode by mutableStateOf(false)
private set
// 临时保存的生成结果,用于在生成过程中暂时隐藏当前结果
private var tempAgentTitle by mutableStateOf("")
private var tempAgentDescription by mutableStateOf("")
// AddAgentViewModel实例用于头像和创建逻辑
private val addAgentViewModel = AddAgentViewModel
// 获取头像相关状态
val croppedBitmap get() = addAgentViewModel.croppedBitmap
val isSelectingAvatar get() = addAgentViewModel.isSelectingAvatar
init {
// 初始化时检查是否需要恢复状态
if (addAgentViewModel.hasExitedPage) {
// 如果之前已经完全退出页面,清空所有数据
addAgentViewModel.clearData()
} else {
// 否则恢复已有状态(包括从头像选择回来的情况)
if (addAgentViewModel.name.isNotEmpty()) {
agentTitle = addAgentViewModel.name
}
if (addAgentViewModel.desc.isNotEmpty()) {
agentDescription = addAgentViewModel.desc
}
// 恢复输入文本
if (addAgentViewModel.generateInputText.isNotEmpty()) {
inputText = addAgentViewModel.generateInputText
}
}
}
fun updateInputText(text: String) {
inputText = text
addAgentViewModel.generateInputText = text // 同时保存到AddAgentViewModel
clearError()
}
fun updateAgentTitle(title: String) {
agentTitle = title
syncToAddAgentViewModel()
clearError()
}
fun updateAgentDescription(description: String) {
agentDescription = description
syncToAddAgentViewModel()
clearError()
}
private fun clearError() {
errorMessage = null
}
private fun syncToAddAgentViewModel() {
addAgentViewModel.name = agentTitle
addAgentViewModel.desc = agentDescription
}
fun setSelectingAvatar(isSelecting: Boolean) {
addAgentViewModel.isSelectingAvatar = isSelecting
}
fun markPageExited() {
addAgentViewModel.hasExitedPage = true
}
fun syncStateOnResume() {
// 如果之前在选择头像,现在回来了,重置选择状态
if (addAgentViewModel.isSelectingAvatar) {
addAgentViewModel.isSelectingAvatar = false
// 从头像选择页面回来,恢复文本状态
if (addAgentViewModel.name.isNotEmpty()) {
agentTitle = addAgentViewModel.name
}
if (addAgentViewModel.desc.isNotEmpty()) {
agentDescription = addAgentViewModel.desc
}
if (addAgentViewModel.generateInputText.isNotEmpty()) {
inputText = addAgentViewModel.generateInputText
}
}
}
fun enableManualMode() {
isManualMode = true
// 手动模式下,如果没有现有内容,初始化为空
if (agentTitle.isEmpty() && agentDescription.isEmpty()) {
agentTitle = ""
agentDescription = ""
}
}
fun disableManualMode() {
isManualMode = false
}
fun generateAgentInfo() {
if (inputText.isBlank() || isGenerating) return
viewModelScope.launch {
try {
isGenerating = true
clearError()
// 开始生成时,暂存当前结果并清空显示
tempAgentTitle = agentTitle
tempAgentDescription = agentDescription
agentTitle = ""
agentDescription = ""
val response = ApiClient.longTimeoutApi.generateAgentInfo(
GenerateAgentInfoRequestBody(inputText)
)
if (response.isSuccessful) {
val data = response.body()?.data
data?.let {
// 成功时,使用新结果
agentTitle = it.title
agentDescription = it.description
syncToAddAgentViewModel()
// 清空临时保存
tempAgentTitle = ""
tempAgentDescription = ""
}
} else {
// 失败时,恢复之前的结果
agentTitle = tempAgentTitle
agentDescription = tempAgentDescription
tempAgentTitle = ""
tempAgentDescription = ""
errorMessage = "生成失败,请重试"
}
} catch (e: Exception) {
// 异常时,恢复之前的结果
agentTitle = tempAgentTitle
agentDescription = tempAgentDescription
tempAgentTitle = ""
tempAgentDescription = ""
errorMessage = "网络错误: ${e.message}"
} finally {
isGenerating = false
}
}
}
fun createAgent(context: Context, onSuccess: () -> Unit) {
if (isCreating) return
viewModelScope.launch {
try {
isCreating = true
clearError()
// 验证输入
val validationError = addAgentViewModel.validate()
if (validationError != null) {
errorMessage = validationError
return@launch
}
// 调用创建智能体API
val result = addAgentViewModel.createAgent(context)
if (result != null) {
// 创建成功,清空数据
clearData()
onSuccess()
} else {
errorMessage = "创建失败,请重试"
}
} catch (e: Exception) {
errorMessage = "创建智能体失败: ${e.message}"
} finally {
isCreating = false
}
}
}
fun clearData() {
inputText = ""
agentTitle = ""
agentDescription = ""
errorMessage = null
isGenerating = false
isCreating = false
addAgentViewModel.clearData()
}
// 检查是否可以创建
fun canCreate(): Boolean {
return !isCreating && agentTitle.isNotBlank() && agentDescription.isNotBlank()
}
// 检查是否可以生成
fun canGenerate(): Boolean {
return !isGenerating && inputText.isNotBlank()
}
// 检查是否有生成结果或处于手动模式
fun hasGeneratedResult(): Boolean {
return agentTitle.isNotEmpty() || agentDescription.isNotEmpty() || isManualMode
}
}

View File

@@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:pathData="m9.098,4.33 l1.475,-1.475a0.643,0.643 0,0 1,0.909 0l1.663,1.663a0.643,0.643 0,0 1,0 0.91L11.67,6.901M9.098,4.33l-6.243,6.242a0.643,0.643 0,0 0,-0.188 0.455v1.663c0,0.355 0.288,0.643 0.643,0.643h1.663c0.17,0 0.334,-0.067 0.455,-0.188l6.242,-6.243M9.098,4.33l2.572,2.572"
android:strokeLineJoin="round"
android:strokeWidth="1.333"
android:fillColor="#00000000"
android:strokeColor="#7C45ED"
android:fillType="evenOdd"
android:strokeLineCap="round"/>
<path
android:pathData="M3.608,6.333c0.13,0 0.202,-0.07 0.215,-0.208 0.056,-0.434 0.114,-0.775 0.172,-1.022 0.06,-0.247 0.152,-0.434 0.28,-0.56 0.128,-0.126 0.318,-0.221 0.57,-0.286 0.252,-0.066 0.599,-0.133 1.042,-0.202 0.152,-0.026 0.228,-0.1 0.228,-0.222 0,-0.06 -0.02,-0.11 -0.059,-0.146a0.304,0.304 0,0 0,-0.143 -0.075,15.533 15.533,0 0,1 -1.048,-0.225c-0.256,-0.067 -0.45,-0.161 -0.583,-0.283 -0.132,-0.121 -0.228,-0.304 -0.287,-0.547a10.18,10.18 0,0 1,-0.172 -1.015c-0.013,-0.14 -0.085,-0.209 -0.215,-0.209a0.221,0.221 0,0 0,-0.153 0.056,0.208 0.208,0 0,0 -0.068,0.146 9.948,9.948 0,0 1,-0.176 1.039c-0.06,0.25 -0.155,0.438 -0.283,0.566 -0.128,0.128 -0.32,0.224 -0.576,0.286 -0.256,0.063 -0.608,0.125 -1.055,0.186a0.251,0.251 0,0 0,-0.143 0.072,0.203 0.203,0 0,0 -0.059,0.15c0,0.06 0.02,0.109 0.059,0.146 0.039,0.037 0.086,0.062 0.143,0.075 0.447,0.082 0.799,0.158 1.055,0.228 0.256,0.069 0.448,0.166 0.576,0.29 0.128,0.123 0.221,0.306 0.28,0.55 0.058,0.242 0.118,0.581 0.179,1.015a0.202,0.202 0,0 0,0.068 0.14,0.221 0.221,0 0,0 0.153,0.055zM12.516,14.702c0.086,0 0.139,-0.05 0.156,-0.15 0.056,-0.308 0.108,-0.552 0.156,-0.732a0.963,0.963 0,0 1,0.208 -0.417,0.879 0.879,0 0,1 0.41,-0.225c0.183,-0.052 0.439,-0.106 0.769,-0.162 0.1,-0.018 0.15,-0.072 0.15,-0.163 0,-0.091 -0.05,-0.146 -0.15,-0.163a6.959,6.959 0,0 1,-0.768 -0.166,0.919 0.919,0 0,1 -0.41,-0.224 0.937,0.937 0,0 1,-0.209 -0.414c-0.048,-0.18 -0.1,-0.426 -0.156,-0.739 -0.017,-0.095 -0.07,-0.143 -0.156,-0.143 -0.091,0 -0.146,0.048 -0.163,0.143a9.897,9.897 0,0 1,-0.156 0.74,0.937 0.937,0 0,1 -0.209,0.413 0.919,0.919 0,0 1,-0.41 0.224c-0.182,0.054 -0.436,0.11 -0.762,0.166 -0.1,0.017 -0.15,0.072 -0.15,0.163 0,0.091 0.05,0.145 0.15,0.163 0.326,0.056 0.58,0.11 0.762,0.162a0.879,0.879 0,0 1,0.41 0.225c0.091,0.098 0.16,0.237 0.209,0.417 0.047,0.18 0.1,0.424 0.156,0.732 0.009,0.044 0.026,0.08 0.052,0.108a0.143,0.143 0,0 0,0.11 0.042z"
android:fillColor="#7C45ED"
android:fillType="evenOdd"/>
</vector>

View File

@@ -0,0 +1,36 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:pathData="M24,24m-24,0a24,24 0,1 1,48 0a24,24 0,1 1,-48 0"
android:fillColor="#5E5CE6"
android:fillAlpha="0.1"
android:fillType="evenOdd"/>
<path
android:pathData="M29.132,11.3c0.861,0.075 1.609,0.484 2.124,1.082a3.019,3.019 50,0 1,-0.378 4.325c-0.448,0.365 -0.987,0.6 -1.56,0.68l-0.02,0.002 0.057,0.035c1.649,0.997 2.914,2.401 3.789,4.009l0.041,0.076c1.115,2.087 1.576,4.508 1.374,6.807 -0.237,2.703 -1.392,4.636 -3.188,5.873a3.07,3.07 50,0 1,0.025 1.749c-0.168,0.58 -0.476,0.756 -0.578,0.791 -0.176,0.059 -1.855,-0.393 -2.957,-1.133 -1.289,0.249 -2.726,0.304 -4.271,0.168 -3.173,-0.276 -6.14,-1.296 -8.154,-3.002 -1.84,-1.559 -2.906,-3.682 -2.672,-6.345 0.261,-2.968 1.606,-5.945 3.885,-7.972 1.978,-1.761 4.657,-2.815 7.951,-2.529 0.608,0.053 1.187,0.148 1.738,0.28l0.039,0.009 -0.01,-0.013a3.013,3.013 50,0 1,-0.627 -2.061l0.004,-0.051c0.074,-0.845 0.492,-1.579 1.109,-2.083a3.163,3.163 50,0 1,2.281 -0.698z"
android:fillType="nonZero">
<aapt:attr name="android:fillColor">
<gradient
android:startX="23.669"
android:startY="11.286"
android:endX="23.669"
android:endY="36.734"
android:type="linear">
<item android:offset="0" android:color="#FF7C45ED"/>
<item android:offset="0.236" android:color="#FF7C68EF"/>
<item android:offset="1" android:color="#FF7BD8F8"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M17.51,22.904L17.736,22.924A0.912,0.912 50,0 1,18.565 23.912L18.419,25.584A0.912,0.912 50,0 1,17.431 26.413L17.205,26.393A0.912,0.912 50,0 1,16.376 25.405L16.522,23.733A0.912,0.912 50,0 1,17.51 22.904z"
android:fillColor="#FFF"
android:fillType="nonZero"/>
<path
android:pathData="M22.163,23.341L22.389,23.361A0.912,0.912 50,0 1,23.218 24.349L23.072,26.021A0.912,0.912 50,0 1,22.083 26.85L21.857,26.83A0.912,0.912 50,0 1,21.028 25.842L21.175,24.17A0.912,0.912 50,0 1,22.163 23.341z"
android:fillColor="#FFF"
android:fillType="nonZero"/>
</vector>

View File

@@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:pathData="m9.185,2 l2.37,2.341 -6.518,6.44h-2.37V8.438zM2.667,13.707h10.667"
android:strokeLineJoin="round"
android:strokeWidth="1.185"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#000"
android:strokeLineCap="round"/>
</vector>