feat: 新增AI智能体编辑功能和群聊搜索
- **AI智能体编辑** - 新增AI智能体编辑页面(`AiPromptEditScreen`),允许创建者修改智能体的头像、名称、描述和公开/私有状态。 - 在AI个人主页为创建者添加入口,可进入编辑页面。 - 新增`updatePrompt`和`getPromptDetail`接口,用于获取和更新智能体信息。 - 完善头像裁剪逻辑,使其同时支持创建和编辑两种模式。 - **群聊搜索** - 在全局搜索中新增“群聊”分类,用户可以搜索公开群聊。 - **优化** - AI个人主页(`AiProfileV3`)数据加载逻辑优化,以正确获取创建者信息。 - 修复了当群聊头像为空时,无法正确显示默认头像的问题。
This commit is contained in:
@@ -69,6 +69,10 @@ data class AccountProfile(
|
|||||||
val aiRoleAvatar: String? = null,
|
val aiRoleAvatar: String? = null,
|
||||||
val aiRoleAvatarMedium: String? = null,
|
val aiRoleAvatarMedium: String? = null,
|
||||||
val aiRoleAvatarLarge: String? = null,
|
val aiRoleAvatarLarge: String? = null,
|
||||||
|
|
||||||
|
// 创建者信息(仅AI账号有)
|
||||||
|
@SerializedName("creatorProfile")
|
||||||
|
val creatorProfile: com.aiosman.ravenow.data.CreatorProfile? = null,
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* 转换为Entity
|
* 转换为Entity
|
||||||
@@ -103,7 +107,8 @@ data class AccountProfile(
|
|||||||
},
|
},
|
||||||
aiRoleAvatarLarge = aiRoleAvatarLarge?.let {
|
aiRoleAvatarLarge = aiRoleAvatarLarge?.let {
|
||||||
if (it.isNotEmpty()) "${ApiClient.BASE_SERVER}$it" else null
|
if (it.isNotEmpty()) "${ApiClient.BASE_SERVER}$it" else null
|
||||||
}
|
},
|
||||||
|
creatorProfile = creatorProfile?.toCreatorProfileEntity()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -331,3 +331,21 @@ class RecommendationServiceImpl : RecommendationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CreatorProfile 扩展函数,转换为 CreatorProfileEntity
|
||||||
|
*/
|
||||||
|
fun CreatorProfile.toCreatorProfileEntity(): com.aiosman.ravenow.entity.CreatorProfileEntity {
|
||||||
|
return com.aiosman.ravenow.entity.CreatorProfileEntity(
|
||||||
|
id = id,
|
||||||
|
username = username,
|
||||||
|
nickname = nickname,
|
||||||
|
avatar = avatar?.let {
|
||||||
|
if (it.isNotEmpty()) "${ApiClient.BASE_SERVER}$it" else null
|
||||||
|
},
|
||||||
|
bio = bio,
|
||||||
|
trtcUserId = trtcUserId,
|
||||||
|
chatAIId = chatAIId,
|
||||||
|
aiAccount = aiAccount
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1500,6 +1500,35 @@ interface RaveNowAPI {
|
|||||||
@Query("pageSize") pageSize: Int? = null
|
@Query("pageSize") pageSize: Int? = null
|
||||||
): Response<ListContainer<Agent>>
|
): Response<ListContainer<Agent>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取Prompt详情(支持ID或OpenId)
|
||||||
|
* @param promptId Prompt ID或OpenId(UUID格式)
|
||||||
|
*/
|
||||||
|
@GET("outside/prompt/{promptId}")
|
||||||
|
suspend fun getPromptDetail(
|
||||||
|
@Path("promptId") promptId: String
|
||||||
|
): Response<DataContainer<Agent>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新Prompt(支持ID或OpenId)
|
||||||
|
* @param promptId Prompt ID或OpenId(UUID格式)
|
||||||
|
* @param avatar 头像文件(可选)
|
||||||
|
* @param title 标题(可选)
|
||||||
|
* @param desc 描述(可选)
|
||||||
|
* @param value 内容(可选)
|
||||||
|
* @param isPublic 是否公开(可选)
|
||||||
|
*/
|
||||||
|
@Multipart
|
||||||
|
@PATCH("outside/prompt/{promptId}")
|
||||||
|
suspend fun updatePrompt(
|
||||||
|
@Path("promptId") promptId: String,
|
||||||
|
@Part avatar: MultipartBody.Part?,
|
||||||
|
@Part("title") title: RequestBody?,
|
||||||
|
@Part("desc") desc: RequestBody?,
|
||||||
|
@Part("value") value: RequestBody?,
|
||||||
|
@Part("public") isPublic: RequestBody?,
|
||||||
|
): Response<DataContainer<Agent>>
|
||||||
|
|
||||||
// ========== Agent Rule API ==========
|
// ========== Agent Rule API ==========
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -72,6 +72,9 @@ data class AccountProfileEntity(
|
|||||||
val aiRoleAvatar: String? = null,
|
val aiRoleAvatar: String? = null,
|
||||||
val aiRoleAvatarMedium: String? = null,
|
val aiRoleAvatarMedium: String? = null,
|
||||||
val aiRoleAvatarLarge: String? = null,
|
val aiRoleAvatarLarge: String? = null,
|
||||||
|
|
||||||
|
// 创建者信息(仅AI账号有)
|
||||||
|
val creatorProfile: CreatorProfileEntity? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -115,6 +118,28 @@ data class NoticeUserEntity(
|
|||||||
val avatar: String,
|
val avatar: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建者信息
|
||||||
|
*/
|
||||||
|
data class CreatorProfileEntity(
|
||||||
|
// 用户ID
|
||||||
|
val id: Long,
|
||||||
|
// 用户名
|
||||||
|
val username: String? = null,
|
||||||
|
// 昵称
|
||||||
|
val nickname: String,
|
||||||
|
// 头像
|
||||||
|
val avatar: String? = null,
|
||||||
|
// 个人简介
|
||||||
|
val bio: String? = null,
|
||||||
|
// trtcUserId
|
||||||
|
val trtcUserId: String? = null,
|
||||||
|
// chatAIId
|
||||||
|
val chatAIId: String? = null,
|
||||||
|
// 是否为AI账号
|
||||||
|
val aiAccount: Boolean = false,
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户点赞消息分页数据加载器
|
* 用户点赞消息分页数据加载器
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
package com.aiosman.ravenow.entity
|
package com.aiosman.ravenow.entity
|
||||||
|
|
||||||
|
import androidx.paging.PagingSource
|
||||||
|
import androidx.paging.PagingState
|
||||||
import com.aiosman.ravenow.data.ListContainer
|
import com.aiosman.ravenow.data.ListContainer
|
||||||
|
import com.aiosman.ravenow.data.Room
|
||||||
import com.aiosman.ravenow.data.ServiceException
|
import com.aiosman.ravenow.data.ServiceException
|
||||||
import com.aiosman.ravenow.data.api.ApiClient
|
import com.aiosman.ravenow.data.api.ApiClient
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 群聊房间
|
* 群聊房间
|
||||||
@@ -253,3 +257,58 @@ class RoomLoader : DataLoader<AgentEntity,AgentLoaderExtraArgs>() {
|
|||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 房间远程数据源
|
||||||
|
*/
|
||||||
|
class RoomRemoteDataSource {
|
||||||
|
suspend fun searchRooms(
|
||||||
|
pageNumber: Int,
|
||||||
|
pageSize: Int = 20,
|
||||||
|
search: String
|
||||||
|
): ListContainer<RoomEntity>? {
|
||||||
|
val resp = ApiClient.api.getRooms(
|
||||||
|
page = pageNumber,
|
||||||
|
pageSize = pageSize,
|
||||||
|
search = search,
|
||||||
|
roomType = "public" // 搜索时只显示公有房间
|
||||||
|
)
|
||||||
|
val body = resp.body() ?: return null
|
||||||
|
return ListContainer(
|
||||||
|
total = body.total,
|
||||||
|
page = pageNumber,
|
||||||
|
pageSize = pageSize,
|
||||||
|
list = body.list.map { it.toRoomtEntity() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 房间搜索分页加载器
|
||||||
|
*/
|
||||||
|
class RoomSearchPagingSource(
|
||||||
|
private val roomRemoteDataSource: RoomRemoteDataSource,
|
||||||
|
private val keyword: String,
|
||||||
|
) : PagingSource<Int, RoomEntity>() {
|
||||||
|
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, RoomEntity> {
|
||||||
|
return try {
|
||||||
|
val currentPage = params.key ?: 1
|
||||||
|
val rooms = roomRemoteDataSource.searchRooms(
|
||||||
|
pageNumber = currentPage,
|
||||||
|
pageSize = params.loadSize,
|
||||||
|
search = keyword
|
||||||
|
)
|
||||||
|
LoadResult.Page(
|
||||||
|
data = rooms?.list ?: listOf(),
|
||||||
|
prevKey = if (currentPage == 1) null else currentPage - 1,
|
||||||
|
nextKey = if (rooms?.list?.isNotEmpty() == true) currentPage + 1 else null
|
||||||
|
)
|
||||||
|
} catch (exception: IOException) {
|
||||||
|
LoadResult.Error(exception)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getRefreshKey(state: PagingState<Int, RoomEntity>): Int? {
|
||||||
|
return state.anchorPosition
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,6 +41,7 @@ import com.aiosman.ravenow.ui.account.ResetPasswordScreen
|
|||||||
import com.aiosman.ravenow.ui.account.ZodiacSelectScreen
|
import com.aiosman.ravenow.ui.account.ZodiacSelectScreen
|
||||||
import com.aiosman.ravenow.ui.agent.AddAgentScreen
|
import com.aiosman.ravenow.ui.agent.AddAgentScreen
|
||||||
import com.aiosman.ravenow.ui.agent.AgentImageCropScreen
|
import com.aiosman.ravenow.ui.agent.AgentImageCropScreen
|
||||||
|
import com.aiosman.ravenow.ui.agent.AiPromptEditScreen
|
||||||
import com.aiosman.ravenow.ui.group.CreateGroupChatScreen
|
import com.aiosman.ravenow.ui.group.CreateGroupChatScreen
|
||||||
import com.aiosman.ravenow.ui.chat.ChatAiScreen
|
import com.aiosman.ravenow.ui.chat.ChatAiScreen
|
||||||
import com.aiosman.ravenow.ui.chat.ChatSettingScreen
|
import com.aiosman.ravenow.ui.chat.ChatSettingScreen
|
||||||
@@ -133,6 +134,7 @@ sealed class NavigationRoute(
|
|||||||
data object MbtiSelect : NavigationRoute("MbtiSelect")
|
data object MbtiSelect : NavigationRoute("MbtiSelect")
|
||||||
data object ZodiacSelect : NavigationRoute("ZodiacSelect")
|
data object ZodiacSelect : NavigationRoute("ZodiacSelect")
|
||||||
data object ScanQr : NavigationRoute("ScanQr")
|
data object ScanQr : NavigationRoute("ScanQr")
|
||||||
|
data object AiPromptEdit : NavigationRoute("AiPromptEdit/{chatAIId}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -684,6 +686,18 @@ fun NavigationController(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
route = NavigationRoute.AiPromptEdit.route,
|
||||||
|
arguments = listOf(navArgument("chatAIId") { type = NavType.StringType })
|
||||||
|
) {
|
||||||
|
val chatAIId = it.arguments?.getString("chatAIId") ?: ""
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalAnimatedContentScope provides this,
|
||||||
|
) {
|
||||||
|
AiPromptEditScreen(chatAIId = chatAIId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ import java.io.InputStream
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 专门用于智能体头像裁剪的页面
|
* 专门用于智能体头像裁剪的页面
|
||||||
|
* 支持创建和编辑两种模式
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun AgentImageCropScreen() {
|
fun AgentImageCropScreen() {
|
||||||
@@ -71,6 +72,14 @@ fun AgentImageCropScreen() {
|
|||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
|
|
||||||
|
// 检查是否在编辑模式(通过检查是否有编辑ViewModel的实例)
|
||||||
|
val isEditMode = remember {
|
||||||
|
// 通过检查导航栈或使用其他方式判断
|
||||||
|
// 暂时使用一个简单的方法:检查AddAgentViewModel是否正在选择头像
|
||||||
|
// 如果不是,则可能是编辑模式
|
||||||
|
!AddAgentViewModel.isSelectingAvatar
|
||||||
|
}
|
||||||
|
|
||||||
val imagePickLauncher = rememberLauncherForActivityResult(
|
val imagePickLauncher = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.GetContent()
|
contract = ActivityResultContracts.GetContent()
|
||||||
) { uri: Uri? ->
|
) { uri: Uri? ->
|
||||||
@@ -84,7 +93,9 @@ fun AgentImageCropScreen() {
|
|||||||
}
|
}
|
||||||
if (uri == null) {
|
if (uri == null) {
|
||||||
// 用户取消选择图片,重置标志
|
// 用户取消选择图片,重置标志
|
||||||
|
if (!isEditMode) {
|
||||||
AddAgentViewModel.isSelectingAvatar = false
|
AddAgentViewModel.isSelectingAvatar = false
|
||||||
|
}
|
||||||
navController.popBackStack()
|
navController.popBackStack()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,7 +133,9 @@ fun AgentImageCropScreen() {
|
|||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
// 用户取消头像选择,重置标志
|
// 用户取消头像选择,重置标志
|
||||||
|
if (!isEditMode) {
|
||||||
AddAgentViewModel.isSelectingAvatar = false
|
AddAgentViewModel.isSelectingAvatar = false
|
||||||
|
}
|
||||||
navController.popBackStack()
|
navController.popBackStack()
|
||||||
},
|
},
|
||||||
colorFilter = ColorFilter.tint(Color.White)
|
colorFilter = ColorFilter.tint(Color.White)
|
||||||
@@ -137,6 +150,12 @@ fun AgentImageCropScreen() {
|
|||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
if (croppedBitmap != null) {
|
if (croppedBitmap != null) {
|
||||||
// 如果已经有裁剪结果,直接返回
|
// 如果已经有裁剪结果,直接返回
|
||||||
|
if (isEditMode) {
|
||||||
|
// 编辑模式:需要找到当前的编辑ViewModel实例
|
||||||
|
// 由于无法直接访问,我们使用一个全局状态或者通过其他方式传递
|
||||||
|
// 暂时先保存到AddAgentViewModel,编辑页面会检查
|
||||||
|
AddAgentViewModel.croppedBitmap = croppedBitmap
|
||||||
|
} else {
|
||||||
AddAgentViewModel.croppedBitmap = croppedBitmap
|
AddAgentViewModel.croppedBitmap = croppedBitmap
|
||||||
// 重置头像选择标志
|
// 重置头像选择标志
|
||||||
AddAgentViewModel.isSelectingAvatar = false
|
AddAgentViewModel.isSelectingAvatar = false
|
||||||
@@ -144,6 +163,8 @@ fun AgentImageCropScreen() {
|
|||||||
AddAgentViewModel.updateAgentAvatar(context)
|
AddAgentViewModel.updateAgentAvatar(context)
|
||||||
navController.popBackStack()
|
navController.popBackStack()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
navController.popBackStack()
|
||||||
} else {
|
} else {
|
||||||
// 进行裁剪
|
// 进行裁剪
|
||||||
imageCrop?.let {
|
imageCrop?.let {
|
||||||
|
|||||||
@@ -0,0 +1,525 @@
|
|||||||
|
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.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.SwitchDefaults
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import com.aiosman.ravenow.LocalNavController
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
|
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.composables.form.FormTextInput
|
||||||
|
import com.aiosman.ravenow.ui.composables.form.FormTextInput2
|
||||||
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI Prompt 编辑页面
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun AiPromptEditScreen(
|
||||||
|
chatAIId: String,
|
||||||
|
viewModel: AiPromptEditViewModel = viewModel()
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
val appColors = LocalAppTheme.current
|
||||||
|
|
||||||
|
// 加载Prompt详情
|
||||||
|
LaunchedEffect(chatAIId) {
|
||||||
|
viewModel.loadPromptDetail(chatAIId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听头像裁剪结果(从AgentImageCropScreen返回)
|
||||||
|
LaunchedEffect(viewModel.isSelectingAvatar) {
|
||||||
|
if (!viewModel.isSelectingAvatar && AddAgentViewModel.croppedBitmap != null) {
|
||||||
|
// 从裁剪页面返回,检查是否有新的裁剪结果
|
||||||
|
viewModel.croppedBitmap = AddAgentViewModel.croppedBitmap
|
||||||
|
// 清空AddAgentViewModel的裁剪结果,避免影响创建页面
|
||||||
|
AddAgentViewModel.croppedBitmap = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态
|
||||||
|
var showPrivacyConfirmDialog by remember { mutableStateOf(false) }
|
||||||
|
var errorMessage by remember { mutableStateOf<String?>(null) }
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
// 计算是否需要付费
|
||||||
|
val needsPayment = viewModel.needsPrivacyPayment()
|
||||||
|
val privacyCost = 100 // 默认100钥匙,后续可以从PointService获取
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(color = appColors.background),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
StatusBarSpacer()
|
||||||
|
|
||||||
|
// 顶部导航栏
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(color = appColors.background)
|
||||||
|
.padding(horizontal = 14.dp, vertical = 16.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.rider_pro_back_icon),
|
||||||
|
contentDescription = "返回",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
navController.navigateUp()
|
||||||
|
},
|
||||||
|
colorFilter = ColorFilter.tint(appColors.text)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(12.dp))
|
||||||
|
Text(
|
||||||
|
"编辑Ai",
|
||||||
|
fontWeight = FontWeight.W600,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
fontSize = 17.sp,
|
||||||
|
color = appColors.text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(1.dp))
|
||||||
|
|
||||||
|
// 内容区域
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.background(appColors.background)
|
||||||
|
) {
|
||||||
|
// 头像选择
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 18.dp),
|
||||||
|
horizontalAlignment = Alignment.Start
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.avatar),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = appColors.text,
|
||||||
|
fontWeight = FontWeight.W600
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(72.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(
|
||||||
|
brush = Brush.linearGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color(0x777c45ed),
|
||||||
|
Color(0x777c68ef),
|
||||||
|
Color(0x557bd8f8)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.noRippleClickable {
|
||||||
|
viewModel.isSelectingAvatar = true
|
||||||
|
// 标记为编辑模式
|
||||||
|
AddAgentViewModel.isSelectingAvatar = false
|
||||||
|
navController.navigate(NavigationRoute.AgentImageCrop.route)
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
when {
|
||||||
|
viewModel.croppedBitmap != null -> {
|
||||||
|
Image(
|
||||||
|
bitmap = viewModel.croppedBitmap!!.asImageBitmap(),
|
||||||
|
contentDescription = "Avatar",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(72.dp)
|
||||||
|
.clip(CircleShape),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
}
|
||||||
|
viewModel.avatarUrl != null -> {
|
||||||
|
CustomAsyncImage(
|
||||||
|
context = context,
|
||||||
|
imageUrl = viewModel.avatarUrl!!,
|
||||||
|
contentDescription = "Avatar",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(72.dp)
|
||||||
|
.clip(CircleShape),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.icons_infor_edit),
|
||||||
|
contentDescription = "Edit",
|
||||||
|
colorFilter = ColorFilter.tint(Color.White),
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(18.dp))
|
||||||
|
|
||||||
|
// 名称输入
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.agent_name),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = appColors.text,
|
||||||
|
fontWeight = FontWeight.W600
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
FormTextInput(
|
||||||
|
value = viewModel.title,
|
||||||
|
hint = stringResource(R.string.agent_name_hint_1),
|
||||||
|
background = appColors.inputBackground2,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) { value ->
|
||||||
|
viewModel.title = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(18.dp))
|
||||||
|
|
||||||
|
// 描述输入
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.agent_desc),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = appColors.text,
|
||||||
|
fontWeight = FontWeight.W600
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
FormTextInput2(
|
||||||
|
value = viewModel.desc,
|
||||||
|
hint = stringResource(R.string.agent_desc_hint),
|
||||||
|
background = appColors.inputBackground2,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) { value ->
|
||||||
|
viewModel.desc = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(18.dp))
|
||||||
|
|
||||||
|
// 设定权限区域
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "设定权限",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = appColors.text,
|
||||||
|
fontWeight = FontWeight.W600
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
// 公开/私有切换
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(appColors.inputBackground2)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = if (viewModel.isPublic) "公开" else "私有",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = appColors.text,
|
||||||
|
fontWeight = FontWeight.W500
|
||||||
|
)
|
||||||
|
Switch(
|
||||||
|
checked = viewModel.isPublic,
|
||||||
|
onCheckedChange = { checked ->
|
||||||
|
if (!checked && needsPayment && !viewModel.paidForPrivacyEdit) {
|
||||||
|
// 需要付费,显示确认对话框
|
||||||
|
showPrivacyConfirmDialog = true
|
||||||
|
} else {
|
||||||
|
viewModel.isPublic = checked
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = SwitchDefaults.colors(
|
||||||
|
checkedThumbColor = Color.White,
|
||||||
|
checkedTrackColor = appColors.brandColorsColor,
|
||||||
|
uncheckedThumbColor = Color.White,
|
||||||
|
uncheckedTrackColor = appColors.brandColorsColor.copy(alpha = 0.5f),
|
||||||
|
uncheckedBorderColor = Color.Transparent
|
||||||
|
),
|
||||||
|
modifier = Modifier.size(width = 64.dp, height = 28.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 首次解锁AI权限提示
|
||||||
|
if (needsPayment && !viewModel.paidForPrivacyEdit) {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(appColors.inputBackground2.copy(alpha = 0.9f))
|
||||||
|
.padding(12.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "首次解锁 AI 权限",
|
||||||
|
fontSize = 13.sp,
|
||||||
|
color = appColors.text,
|
||||||
|
fontWeight = FontWeight.W500
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "将消耗",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = appColors.text.copy(alpha = 0.6f)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text(
|
||||||
|
text = "$privacyCost",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = appColors.brandColorsColor,
|
||||||
|
fontWeight = FontWeight.W500
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(2.dp))
|
||||||
|
Text(
|
||||||
|
text = "钥匙",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = appColors.brandColorsColor,
|
||||||
|
fontWeight = FontWeight.W500
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text(
|
||||||
|
text = "解锁后可随时切换",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = appColors.text.copy(alpha = 0.6f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(18.dp))
|
||||||
|
|
||||||
|
// 添加智能体记忆按钮
|
||||||
|
AddAgentMemoryButton(
|
||||||
|
onAddMemoryClick = {
|
||||||
|
// TODO: 导航到记忆管理页面
|
||||||
|
// navController.navigate(NavigationRoute.AgentMemoryManage.route.replace("{chatAIId}", chatAIId))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 底部保存按钮
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(color = appColors.background)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 16.dp)
|
||||||
|
) {
|
||||||
|
ActionButton(
|
||||||
|
text = "保存",
|
||||||
|
enabled = !viewModel.isUpdating && !viewModel.isLoading,
|
||||||
|
isLoading = viewModel.isUpdating,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
// 验证输入
|
||||||
|
val validationError = viewModel.validate()
|
||||||
|
if (validationError != null) {
|
||||||
|
errorMessage = validationError
|
||||||
|
return@ActionButton
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否需要付费确认
|
||||||
|
if (needsPayment && !viewModel.paidForPrivacyEdit) {
|
||||||
|
showPrivacyConfirmDialog = true
|
||||||
|
return@ActionButton
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行保存
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
viewModel.updatePrompt(context)
|
||||||
|
navController.navigateUp()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
errorMessage = e.message ?: "保存失败"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 隐私权限付费确认对话框
|
||||||
|
if (showPrivacyConfirmDialog) {
|
||||||
|
// TODO: 实现付费确认对话框
|
||||||
|
// 暂时直接切换,后续可以添加积分检查和扣减逻辑
|
||||||
|
androidx.compose.material3.AlertDialog(
|
||||||
|
onDismissRequest = { showPrivacyConfirmDialog = false },
|
||||||
|
title = { Text("升级隐私权限") },
|
||||||
|
text = { Text("首次切换智能体的公开/私有状态需要支付一次性费用。支付后可自由在公有/私有之间切换,后续不再扣费。") },
|
||||||
|
confirmButton = {
|
||||||
|
androidx.compose.material3.TextButton(
|
||||||
|
onClick = {
|
||||||
|
showPrivacyConfirmDialog = false
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
viewModel.isPublic = false
|
||||||
|
viewModel.updatePrompt(context)
|
||||||
|
viewModel.paidForPrivacyEdit = true
|
||||||
|
navController.navigateUp()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
errorMessage = e.message ?: "保存失败"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text("确认支付")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
androidx.compose.material3.TextButton(
|
||||||
|
onClick = { showPrivacyConfirmDialog = false }
|
||||||
|
) {
|
||||||
|
Text("取消")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 错误提示
|
||||||
|
errorMessage?.let { error ->
|
||||||
|
LaunchedEffect(error) {
|
||||||
|
kotlinx.coroutines.delay(3000)
|
||||||
|
errorMessage = null
|
||||||
|
}
|
||||||
|
// TODO: 显示Toast或Snackbar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AddAgentMemoryButton(
|
||||||
|
onAddMemoryClick: () -> Unit
|
||||||
|
) {
|
||||||
|
val appColors = LocalAppTheme.current
|
||||||
|
|
||||||
|
// 定义渐变边框颜色:紫色到蓝色
|
||||||
|
val borderGradient = Brush.horizontalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color(0xFF7C45ED), // 紫色
|
||||||
|
Color(0xFF4A90E2) // 蓝色
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 浅紫色背景
|
||||||
|
val lightPurpleBackground = Color(0xFFF5F0FF)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
// 使用两层Box来实现渐变边框效果
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(brush = borderGradient)
|
||||||
|
.padding(1.5.dp) // 边框宽度
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(lightPurpleBackground)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 14.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
onAddMemoryClick()
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
// 脑部图标
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.ic_brain_add),
|
||||||
|
contentDescription = "添加智能体记忆",
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
colorFilter = ColorFilter.tint(Color(0xFF7C45ED))
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
|
||||||
|
// 文字
|
||||||
|
Text(
|
||||||
|
text = "添加智能体记忆",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.W600,
|
||||||
|
color = Color(0xFF7C45ED)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
package com.aiosman.ravenow.ui.agent
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.util.Log
|
||||||
|
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.UploadImage
|
||||||
|
import com.aiosman.ravenow.data.ServiceException
|
||||||
|
import com.aiosman.ravenow.data.api.ApiClient
|
||||||
|
import com.aiosman.ravenow.entity.AgentEntity
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
|
import okhttp3.MultipartBody
|
||||||
|
import okhttp3.RequestBody.Companion.asRequestBody
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class AiPromptEditViewModel : ViewModel() {
|
||||||
|
var chatAIId by mutableStateOf("")
|
||||||
|
var title by mutableStateOf("")
|
||||||
|
var desc by mutableStateOf("")
|
||||||
|
var isPublic by mutableStateOf(true)
|
||||||
|
var originalIsPublic by mutableStateOf(true)
|
||||||
|
var paidForPrivacyEdit by mutableStateOf(false)
|
||||||
|
var avatarUrl by mutableStateOf<String?>(null)
|
||||||
|
var croppedBitmap by mutableStateOf<Bitmap?>(null)
|
||||||
|
var isUpdating by mutableStateOf(false)
|
||||||
|
var isLoading by mutableStateOf(false)
|
||||||
|
var errorMessage by mutableStateOf<String?>(null)
|
||||||
|
var isSelectingAvatar by mutableStateOf(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载Prompt详情
|
||||||
|
*/
|
||||||
|
fun loadPromptDetail(chatAIId: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
isLoading = true
|
||||||
|
errorMessage = null
|
||||||
|
this@AiPromptEditViewModel.chatAIId = chatAIId
|
||||||
|
|
||||||
|
val response = ApiClient.api.getPromptDetail(chatAIId)
|
||||||
|
val body = response.body()?.data ?: throw ServiceException("Failed to get prompt detail")
|
||||||
|
|
||||||
|
// 填充数据
|
||||||
|
title = body.title
|
||||||
|
desc = body.desc
|
||||||
|
isPublic = body.isPublic
|
||||||
|
originalIsPublic = body.isPublic
|
||||||
|
avatarUrl = "${ApiClient.BASE_API_URL}/outside${body.avatar}?token=${com.aiosman.ravenow.AppStore.token}"
|
||||||
|
|
||||||
|
// 注意:Agent数据模型可能没有paidForPrivacyEdit字段,需要从其他地方获取
|
||||||
|
// 暂时设为false,后续可以根据实际API响应调整
|
||||||
|
paidForPrivacyEdit = false
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("AiPromptEditViewModel", "Error loading prompt detail", e)
|
||||||
|
errorMessage = "加载失败: ${e.message}"
|
||||||
|
} finally {
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新Prompt
|
||||||
|
*/
|
||||||
|
suspend fun updatePrompt(context: Context): AgentEntity? {
|
||||||
|
try {
|
||||||
|
isUpdating = true
|
||||||
|
errorMessage = null
|
||||||
|
|
||||||
|
// 准备头像文件
|
||||||
|
val avatarFile = if (croppedBitmap != null) {
|
||||||
|
val file = File(context.cacheDir, "agent_avatar_edit.jpg")
|
||||||
|
croppedBitmap!!.compress(Bitmap.CompressFormat.JPEG, 100, file.outputStream())
|
||||||
|
UploadImage(file, "agent_avatar_edit.jpg", "", "jpg")
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 准备请求参数
|
||||||
|
val textTitle = title.trim().toRequestBody("text/plain".toMediaTypeOrNull())
|
||||||
|
val textDesc = desc.trim().toRequestBody("text/plain".toMediaTypeOrNull())
|
||||||
|
val textValue = desc.trim().toRequestBody("text/plain".toMediaTypeOrNull()) // value通常和desc相同
|
||||||
|
val isPublicBody = isPublic.toString().toRequestBody("text/plain".toMediaTypeOrNull())
|
||||||
|
|
||||||
|
val avatarPart: MultipartBody.Part? = avatarFile?.let {
|
||||||
|
val requestFile = it.file.asRequestBody("image/*".toMediaTypeOrNull())
|
||||||
|
MultipartBody.Part.createFormData("avatar", it.filename, requestFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用更新API
|
||||||
|
val response = ApiClient.api.updatePrompt(
|
||||||
|
promptId = chatAIId,
|
||||||
|
avatar = avatarPart,
|
||||||
|
title = textTitle,
|
||||||
|
desc = textDesc,
|
||||||
|
value = textValue,
|
||||||
|
isPublic = isPublicBody
|
||||||
|
)
|
||||||
|
|
||||||
|
val body = response.body()?.data ?: throw ServiceException("Failed to update prompt")
|
||||||
|
|
||||||
|
// 更新本地状态
|
||||||
|
originalIsPublic = isPublic
|
||||||
|
|
||||||
|
return body.toAgentEntity()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("AiPromptEditViewModel", "Error updating prompt", e)
|
||||||
|
errorMessage = "更新失败: ${e.message}"
|
||||||
|
throw e
|
||||||
|
} finally {
|
||||||
|
isUpdating = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证输入
|
||||||
|
*/
|
||||||
|
fun validate(): String? {
|
||||||
|
return when {
|
||||||
|
title.trim().isEmpty() -> "智能体名称不能为空"
|
||||||
|
title.trim().length < 2 -> "智能体名称长度不能少于2个字符"
|
||||||
|
title.trim().length > 20 -> "智能体名称长度不能超过20个字符"
|
||||||
|
desc.trim().isEmpty() -> "智能体描述不能为空"
|
||||||
|
desc.trim().length > 512 -> "智能体描述长度不能超过512个字符"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否需要付费解锁隐私切换
|
||||||
|
*/
|
||||||
|
fun needsPrivacyPayment(): Boolean {
|
||||||
|
// 如果已经解锁过,则不需要付费
|
||||||
|
if (paidForPrivacyEdit) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// 只有从公开(true)切换到私有(false)才需要付费
|
||||||
|
return originalIsPublic == true && isPublic == false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空数据
|
||||||
|
*/
|
||||||
|
fun clearData() {
|
||||||
|
chatAIId = ""
|
||||||
|
title = ""
|
||||||
|
desc = ""
|
||||||
|
isPublic = true
|
||||||
|
originalIsPublic = true
|
||||||
|
paidForPrivacyEdit = false
|
||||||
|
avatarUrl = null
|
||||||
|
croppedBitmap = null
|
||||||
|
isUpdating = false
|
||||||
|
isLoading = false
|
||||||
|
errorMessage = null
|
||||||
|
isSelectingAvatar = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -54,6 +54,8 @@ import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
|
|||||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
import com.aiosman.ravenow.ui.navigateToGroupChat
|
import com.aiosman.ravenow.ui.navigateToGroupChat
|
||||||
import com.aiosman.ravenow.AppStore
|
import com.aiosman.ravenow.AppStore
|
||||||
|
import com.aiosman.ravenow.data.api.ApiClient
|
||||||
|
import android.util.Base64
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -209,7 +211,12 @@ fun RoomItem(
|
|||||||
val avatarUrl = if (room.avatar.isNotEmpty()) {
|
val avatarUrl = if (room.avatar.isNotEmpty()) {
|
||||||
"${ConstVars.BASE_SERVER}/api/v1/outside/${room.avatar}?token=${AppStore.token}"
|
"${ConstVars.BASE_SERVER}/api/v1/outside/${room.avatar}?token=${AppStore.token}"
|
||||||
} else {
|
} else {
|
||||||
""
|
// 如果头像为空,使用群头像接口
|
||||||
|
val groupIdBase64 = Base64.encodeToString(
|
||||||
|
room.trtcType.toByteArray(),
|
||||||
|
Base64.NO_WRAP
|
||||||
|
)
|
||||||
|
"${ApiClient.RETROFIT_URL}group/avatar?groupIdBase64=${groupIdBase64}&token=${AppStore.token}"
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
@@ -74,7 +74,9 @@ import com.aiosman.ravenow.ui.composables.MomentCard
|
|||||||
import com.aiosman.ravenow.ui.composables.TabItem
|
import com.aiosman.ravenow.ui.composables.TabItem
|
||||||
import com.aiosman.ravenow.ui.composables.TabSpacer
|
import com.aiosman.ravenow.ui.composables.TabSpacer
|
||||||
import com.aiosman.ravenow.ui.index.tabs.message.tab.AgentChatListViewModel
|
import com.aiosman.ravenow.ui.index.tabs.message.tab.AgentChatListViewModel
|
||||||
|
import com.aiosman.ravenow.ui.index.tabs.profile.composable.RoomItem
|
||||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
|
import com.aiosman.ravenow.ui.navigateToGroupChat
|
||||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import com.aiosman.ravenow.utils.NetworkUtils
|
import com.aiosman.ravenow.utils.NetworkUtils
|
||||||
@@ -88,7 +90,7 @@ fun SearchScreen() {
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val model = SearchViewModel
|
val model = SearchViewModel
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val pagerState = rememberPagerState(pageCount = { 3 })
|
val pagerState = rememberPagerState(pageCount = { 4 })
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
val systemUiController = rememberSystemUiController()
|
val systemUiController = rememberSystemUiController()
|
||||||
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
|
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
|
||||||
@@ -168,8 +170,9 @@ fun SearchScreen() {
|
|||||||
onClick = { term ->
|
onClick = { term ->
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
keyboardController?.hide()
|
keyboardController?.hide()
|
||||||
|
model.searchText = term
|
||||||
model.onTextChanged(term)
|
model.onTextChanged(term)
|
||||||
pagerState.scrollToPage(0)
|
pagerState.animateScrollToPage(0)
|
||||||
model.search()
|
model.search()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -222,6 +225,18 @@ fun SearchScreen() {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
TabSpacer()
|
||||||
|
Box {
|
||||||
|
TabItem(
|
||||||
|
text = stringResource(R.string.chat_group),
|
||||||
|
isSelected = pagerState.currentPage == 3,
|
||||||
|
onClick = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
pagerState.animateScrollToPage(3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -376,6 +391,7 @@ fun SearchPager(
|
|||||||
0 -> MomentResultTab()
|
0 -> MomentResultTab()
|
||||||
1 -> UserResultTab()
|
1 -> UserResultTab()
|
||||||
2 -> AiResultTab()
|
2 -> AiResultTab()
|
||||||
|
3 -> RoomResultTab()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -753,6 +769,100 @@ fun AiResultTab() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@Composable
|
||||||
|
fun RoomResultTab() {
|
||||||
|
val model = SearchViewModel
|
||||||
|
val rooms = model.roomsFlow.collectAsLazyPagingItems()
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(AppColors.background)
|
||||||
|
) {
|
||||||
|
if (rooms.itemCount == 0 && model.showResult) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
|
||||||
|
|
||||||
|
if (isNetworkAvailable) {
|
||||||
|
androidx.compose.foundation.Image(
|
||||||
|
painter = painterResource(
|
||||||
|
id = if (AppState.darkMode) R.mipmap.syss_yh_qs_as_img
|
||||||
|
else R.mipmap.invalid_name_1
|
||||||
|
),
|
||||||
|
contentDescription = "No Result",
|
||||||
|
modifier = Modifier.size(140.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "咦,什么都没找到...",
|
||||||
|
color = LocalAppTheme.current.text,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.W600
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "换个关键词试试吧,也许会有新发现!",
|
||||||
|
color = LocalAppTheme.current.secondaryText,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.W400
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
androidx.compose.foundation.Image(
|
||||||
|
painter = painterResource(id = R.mipmap.invalid_name_10),
|
||||||
|
contentDescription = "network error",
|
||||||
|
modifier = Modifier.size(140.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.friend_chat_no_network_title),
|
||||||
|
color = LocalAppTheme.current.text,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.W600
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.friend_chat_no_network_subtitle),
|
||||||
|
color = LocalAppTheme.current.secondaryText,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.W400
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(16.dp))
|
||||||
|
ReloadButton(
|
||||||
|
onClick = {
|
||||||
|
SearchViewModel.ResetModel()
|
||||||
|
SearchViewModel.search()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
) {
|
||||||
|
items(rooms.itemCount) { idx ->
|
||||||
|
val roomItem = rooms[idx] ?: return@items
|
||||||
|
RoomItem(
|
||||||
|
room = roomItem,
|
||||||
|
onRoomClick = { roomEntity ->
|
||||||
|
navController.navigateToGroupChat(
|
||||||
|
id = roomEntity.trtcRoomId,
|
||||||
|
name = roomEntity.name,
|
||||||
|
avatar = roomEntity.avatar
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ReloadButton(
|
fun ReloadButton(
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ import com.aiosman.ravenow.entity.AgentEntity
|
|||||||
import com.aiosman.ravenow.entity.AgentRemoteDataSource
|
import com.aiosman.ravenow.entity.AgentRemoteDataSource
|
||||||
import com.aiosman.ravenow.entity.AgentSearchPagingSource
|
import com.aiosman.ravenow.entity.AgentSearchPagingSource
|
||||||
import com.aiosman.ravenow.entity.AgentServiceImpl
|
import com.aiosman.ravenow.entity.AgentServiceImpl
|
||||||
|
import com.aiosman.ravenow.entity.RoomEntity
|
||||||
|
import com.aiosman.ravenow.entity.RoomRemoteDataSource
|
||||||
|
import com.aiosman.ravenow.entity.RoomSearchPagingSource
|
||||||
import com.aiosman.ravenow.utils.SearchHistoryStore
|
import com.aiosman.ravenow.utils.SearchHistoryStore
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
@@ -40,6 +43,8 @@ object SearchViewModel : ViewModel() {
|
|||||||
val usersFlow = _usersFlow.asStateFlow()
|
val usersFlow = _usersFlow.asStateFlow()
|
||||||
private val _agentsFlow = MutableStateFlow<PagingData<AgentEntity>>(PagingData.empty())
|
private val _agentsFlow = MutableStateFlow<PagingData<AgentEntity>>(PagingData.empty())
|
||||||
val agentsFlow = _agentsFlow.asStateFlow()
|
val agentsFlow = _agentsFlow.asStateFlow()
|
||||||
|
private val _roomsFlow = MutableStateFlow<PagingData<RoomEntity>>(PagingData.empty())
|
||||||
|
val roomsFlow = _roomsFlow.asStateFlow()
|
||||||
private lateinit var historyStore: SearchHistoryStore
|
private lateinit var historyStore: SearchHistoryStore
|
||||||
private val _historyFlow = MutableStateFlow<List<String>>(emptyList())
|
private val _historyFlow = MutableStateFlow<List<String>>(emptyList())
|
||||||
val historyFlow = _historyFlow.asStateFlow()
|
val historyFlow = _historyFlow.asStateFlow()
|
||||||
@@ -108,6 +113,19 @@ object SearchViewModel : ViewModel() {
|
|||||||
_agentsFlow.value = it
|
_agentsFlow.value = it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
Pager(
|
||||||
|
config = PagingConfig(pageSize = 5, enablePlaceholders = false),
|
||||||
|
pagingSourceFactory = {
|
||||||
|
RoomSearchPagingSource(
|
||||||
|
RoomRemoteDataSource(),
|
||||||
|
keyword = searchText
|
||||||
|
)
|
||||||
|
}
|
||||||
|
).flow.cachedIn(viewModelScope).collectLatest {
|
||||||
|
_roomsFlow.value = it
|
||||||
|
}
|
||||||
|
}
|
||||||
showResult = true
|
showResult = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,6 +171,7 @@ object SearchViewModel : ViewModel() {
|
|||||||
_momentsFlow.value = PagingData.empty()
|
_momentsFlow.value = PagingData.empty()
|
||||||
_usersFlow.value = PagingData.empty()
|
_usersFlow.value = PagingData.empty()
|
||||||
_agentsFlow.value = PagingData.empty()
|
_agentsFlow.value = PagingData.empty()
|
||||||
|
_roomsFlow.value = PagingData.empty()
|
||||||
showResult = false
|
showResult = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.aiosman.ravenow.ui.profile
|
package com.aiosman.ravenow.ui.profile
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
@@ -10,16 +11,21 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
|||||||
import androidx.compose.foundation.lazy.grid.itemsIndexed
|
import androidx.compose.foundation.lazy.grid.itemsIndexed
|
||||||
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.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
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.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
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.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
@@ -55,7 +61,16 @@ fun AiProfileV3(
|
|||||||
onComment: (MomentEntity) -> Unit = {},
|
onComment: (MomentEntity) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val appColors = LocalAppTheme.current
|
val appColors = LocalAppTheme.current
|
||||||
|
val navController = LocalNavController.current
|
||||||
val numberFormat = remember { NumberFormat.getNumberInstance(Locale.getDefault()) }
|
val numberFormat = remember { NumberFormat.getNumberInstance(Locale.getDefault()) }
|
||||||
|
var showMenu by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
// 判断是否是创建者
|
||||||
|
val isCreator = remember(profile) {
|
||||||
|
profile?.creatorProfile?.id?.let { creatorId ->
|
||||||
|
AppState.UserId?.toLong() == creatorId
|
||||||
|
} ?: false
|
||||||
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -140,12 +155,42 @@ fun AiProfileV3(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 顶部返回按钮
|
// 顶部返回按钮
|
||||||
TopBar()
|
TopBar(
|
||||||
|
onMenuClick = { showMenu = true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 底部菜单
|
||||||
|
if (showMenu) {
|
||||||
|
AiProfileMenuModal(
|
||||||
|
onDismiss = { showMenu = false },
|
||||||
|
onChatClick = {
|
||||||
|
showMenu = false
|
||||||
|
onChatClick()
|
||||||
|
},
|
||||||
|
onShareClick = {
|
||||||
|
showMenu = false
|
||||||
|
onShareClick()
|
||||||
|
},
|
||||||
|
onEditClick = {
|
||||||
|
showMenu = false
|
||||||
|
// 导航到编辑页面
|
||||||
|
profile?.chatAIId?.let { chatAIId ->
|
||||||
|
navController.navigate(
|
||||||
|
NavigationRoute.AiPromptEdit.route.replace("{chatAIId}", chatAIId)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
profile = profile,
|
||||||
|
showEdit = isCreator
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun TopBar() {
|
private fun TopBar(
|
||||||
|
onMenuClick: () -> Unit = {}
|
||||||
|
) {
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
val appColors = LocalAppTheme.current
|
val appColors = LocalAppTheme.current
|
||||||
|
|
||||||
@@ -182,6 +227,25 @@ private fun TopBar() {
|
|||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 菜单按钮
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(40.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(Color.Black.copy(alpha = 0.3f))
|
||||||
|
.noRippleClickable {
|
||||||
|
onMenuClick()
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.rider_pro_more_horizon),
|
||||||
|
contentDescription = "Menu",
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
colorFilter = ColorFilter.tint(Color.White)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -454,3 +518,130 @@ private fun AiProfileActions(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun AiProfileMenuModal(
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onChatClick: () -> Unit,
|
||||||
|
onShareClick: () -> Unit,
|
||||||
|
onEditClick: () -> Unit,
|
||||||
|
showEdit: Boolean = false,
|
||||||
|
profile: AccountProfileEntity? = null
|
||||||
|
) {
|
||||||
|
val appColors = LocalAppTheme.current
|
||||||
|
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
sheetState = sheetState,
|
||||||
|
containerColor = appColors.background,
|
||||||
|
dragHandle = {},
|
||||||
|
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(if (showEdit) 240.dp else 160.dp)
|
||||||
|
.background(appColors.background)
|
||||||
|
.padding(vertical = 47.dp, horizontal = 20.dp)
|
||||||
|
) {
|
||||||
|
// 私信选项
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(end = 16.dp),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(CircleShape)
|
||||||
|
.noRippleClickable {
|
||||||
|
onChatClick()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.rider_pro_message),
|
||||||
|
contentDescription = "",
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
colorFilter = ColorFilter.tint(appColors.text)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.chat_upper),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = appColors.text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分享选项
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(end = if (showEdit) 16.dp else 0.dp),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(CircleShape)
|
||||||
|
.noRippleClickable {
|
||||||
|
onShareClick()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.rider_pro_share),
|
||||||
|
contentDescription = "",
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
colorFilter = ColorFilter.tint(appColors.text)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.share),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = appColors.text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑选项(仅创建者可见)
|
||||||
|
if (showEdit) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(CircleShape)
|
||||||
|
.noRippleClickable {
|
||||||
|
onEditClick()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.group_info_edit),
|
||||||
|
contentDescription = "",
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
colorFilter = ColorFilter.tint(appColors.text)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.edit_profile),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = appColors.text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,8 +44,12 @@ class AiProfileViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
profile = userService.getUserProfile(id)
|
// 先通过用户ID获取基本信息,获取chatAIId
|
||||||
|
val basicProfile = userService.getUserProfile(id)
|
||||||
profileId = id.toInt()
|
profileId = id.toInt()
|
||||||
|
|
||||||
|
// 使用chatAIId通过getUserProfileByOpenId获取完整信息(包含creatorProfile)
|
||||||
|
profile = userService.getUserProfileByOpenId(basicProfile.chatAIId)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("AiProfileViewModel", "Error loading profile", e)
|
Log.e("AiProfileViewModel", "Error loading profile", e)
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
|
|||||||
20
app/src/main/res/drawable/ic_brain_add.xml
Normal file
20
app/src/main/res/drawable/ic_brain_add.xml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<!-- 脑部轮廓 - 更简洁的设计 -->
|
||||||
|
<path
|
||||||
|
android:pathData="M12,4C9.2,4 7,6.2 7,9C7,10.2 7.4,11.3 8,12.1C7.6,12.7 7.3,13.4 7.3,14.1C7.3,15.7 8.6,17 10.2,17C10.6,17 11,16.9 11.3,16.7C11.7,17.3 12.4,17.7 13.2,17.7C14.5,17.7 15.5,16.7 15.5,15.4C15.5,15 15.4,14.6 15.2,14.3C15.7,14 16,13.4 16,12.8C16,11.8 15.4,11 14.5,10.7C14.7,10.2 14.8,9.6 14.8,9C14.8,6.2 12.6,4 9.8,4H12Z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="1.8"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#7C45ED"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<!-- 加号 - 位于右上角 -->
|
||||||
|
<path
|
||||||
|
android:pathData="M17,7h-1.5v1.5h-1.5v1.5h1.5v1.5h1.5v-1.5h1.5v-1.5h-1.5v-1.5z"
|
||||||
|
android:fillColor="#7C45ED"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
</vector>
|
||||||
Reference in New Issue
Block a user