diff --git a/app/src/main/java/com/aiosman/ravenow/data/RoomService.kt b/app/src/main/java/com/aiosman/ravenow/data/RoomService.kt index 8ffda1f..c2d0e58 100644 --- a/app/src/main/java/com/aiosman/ravenow/data/RoomService.kt +++ b/app/src/main/java/com/aiosman/ravenow/data/RoomService.kt @@ -468,7 +468,12 @@ fun RoomRuleCreator.toRoomRuleCreatorEntity(): RoomRuleCreatorEntity { return RoomRuleCreatorEntity( id = id, nickname = nickname, - avatar = avatar + avatar = avatar, + avatarMedium = avatarMedium, + avatarLarge = avatarLarge, + avatarDirectUrl = avatarDirectUrl, + avatarMediumDirectUrl = avatarMediumDirectUrl, + avatarLargeDirectUrl = avatarLargeDirectUrl ) } diff --git a/app/src/main/java/com/aiosman/ravenow/data/api/RiderProAPI.kt b/app/src/main/java/com/aiosman/ravenow/data/api/RiderProAPI.kt index 83d60f4..961b182 100644 --- a/app/src/main/java/com/aiosman/ravenow/data/api/RiderProAPI.kt +++ b/app/src/main/java/com/aiosman/ravenow/data/api/RiderProAPI.kt @@ -913,6 +913,11 @@ data class UpdateRoomRuleRequestBody( * @param id 创建者ID * @param nickname 创建者昵称 * @param avatar 创建者头像文件名 + * @param avatarMedium 中等头像文件名 + * @param avatarLarge 大头像文件名 + * @param avatarDirectUrl 小头像直接访问URL + * @param avatarMediumDirectUrl 中等头像直接访问URL + * @param avatarLargeDirectUrl 大头像直接访问URL */ data class RoomRuleCreator( @SerializedName("id") @@ -920,7 +925,17 @@ data class RoomRuleCreator( @SerializedName("nickname") val nickname: String, @SerializedName("avatar") - val avatar: String + val avatar: String, + @SerializedName("avatarMedium") + val avatarMedium: String? = null, + @SerializedName("avatarLarge") + val avatarLarge: String? = null, + @SerializedName("avatarDirectUrl") + val avatarDirectUrl: String? = null, + @SerializedName("avatarMediumDirectUrl") + val avatarMediumDirectUrl: String? = null, + @SerializedName("avatarLargeDirectUrl") + val avatarLargeDirectUrl: String? = null ) /** diff --git a/app/src/main/java/com/aiosman/ravenow/entity/Group.kt b/app/src/main/java/com/aiosman/ravenow/entity/Group.kt index 0f11a02..e04e80c 100644 --- a/app/src/main/java/com/aiosman/ravenow/entity/Group.kt +++ b/app/src/main/java/com/aiosman/ravenow/entity/Group.kt @@ -13,4 +13,6 @@ data class GroupInfo( val groupAvatar: String, val memberCount: Int, val isCreator: Boolean = false, + val trtcType: String = "Public", + val privateFeePaid: Boolean = false, ) \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/ravenow/entity/Room.kt b/app/src/main/java/com/aiosman/ravenow/entity/Room.kt index 0801c4f..6f932b0 100644 --- a/app/src/main/java/com/aiosman/ravenow/entity/Room.kt +++ b/app/src/main/java/com/aiosman/ravenow/entity/Room.kt @@ -80,7 +80,12 @@ data class ProfileEntity( data class RoomRuleCreatorEntity( val id: Int, val nickname: String, - val avatar: String + val avatar: String, + val avatarMedium: String? = null, + val avatarLarge: String? = null, + val avatarDirectUrl: String? = null, + val avatarMediumDirectUrl: String? = null, + val avatarLargeDirectUrl: String? = null ) /** diff --git a/app/src/main/java/com/aiosman/ravenow/ui/group/GroupChatInfoScreen.kt b/app/src/main/java/com/aiosman/ravenow/ui/group/GroupChatInfoScreen.kt index d0bdfbd..b10fd7b 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/group/GroupChatInfoScreen.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/group/GroupChatInfoScreen.kt @@ -40,6 +40,7 @@ import com.aiosman.ravenow.LocalAppTheme import com.aiosman.ravenow.LocalNavController import com.aiosman.ravenow.R import com.aiosman.ravenow.ui.composables.CustomAsyncImage +import com.aiosman.ravenow.ui.composables.PointsPaymentDialog import com.aiosman.ravenow.ui.composables.StatusBarSpacer import com.aiosman.ravenow.ui.index.NavItem import com.aiosman.ravenow.ui.modifiers.noRippleClickable @@ -66,6 +67,8 @@ fun GroupChatInfoScreen(groupId: String) { var showAddMemoryDialog by remember { mutableStateOf(false) } var showMemoryManageDialog by remember { mutableStateOf(false) } var showVisibilityDialog by remember { mutableStateOf(false) } + var showVisibilityPaymentDialog by remember { mutableStateOf(false) } + var pendingIsPrivate by remember { mutableStateOf(false) } val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val memoryManageSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) @@ -287,27 +290,6 @@ fun GroupChatInfoScreen(groupId: String) { } } - // 解锁群扩展 横幅 - item { - Spacer(modifier = Modifier.height(12.dp)) - Box( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(10.dp)) - .background(AppColors.decentBackground.copy(alpha = 0.35f)) - .padding(horizontal = 10.dp, vertical = 10.dp), - contentAlignment = Alignment.CenterStart - ) { - Text( - text = stringResource(R.string.group_chat_info_unlock_extension), - style = androidx.compose.ui.text.TextStyle( - color = AppColors.main, - fontSize = 12.sp, - fontWeight = FontWeight.Medium - ) - ) - } - } // 群记忆 卡片 item { @@ -457,6 +439,8 @@ fun GroupChatInfoScreen(groupId: String) { ), modifier = Modifier.weight(1f) ) + // 未解锁时才显示"待解锁" + if (viewModel.groupInfo?.privateFeePaid != true) { Text( text = stringResource(R.string.group_chat_info_locked), style = androidx.compose.ui.text.TextStyle( @@ -464,6 +448,7 @@ fun GroupChatInfoScreen(groupId: String) { fontSize = 11.sp ) ) + } Image( painter = painterResource(R.drawable.rave_now_nav_right), modifier = Modifier.size(16.dp), @@ -592,11 +577,50 @@ fun GroupChatInfoScreen(groupId: String) { shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp) ) { GroupVisibilityDialog( - onDismiss = { showVisibilityDialog = false } + viewModel = viewModel, + onDismiss = { showVisibilityDialog = false }, + onConfirmPrivate = { isPrivate -> + // 如果选择私密群组且未解锁,显示付费确认弹框 + if (isPrivate && (viewModel.groupInfo?.privateFeePaid != true)) { + pendingIsPrivate = true + showVisibilityDialog = false + showVisibilityPaymentDialog = true + } else { + // 直接更新可见性 + viewModel.updateVisibility(isPrivate) + showVisibilityDialog = false + } + } ) } } + // 付费确认弹框 + if (showVisibilityPaymentDialog) { + val cost = viewModel.privateGroupCost ?: 0 + val currentBalance = viewModel.pointsBalance ?: 0 + val balanceAfterCost = (currentBalance - cost).coerceAtLeast(0) + val isBalanceSufficient = currentBalance >= cost + + PointsPaymentDialog( + cost = cost, + currentBalance = currentBalance, + balanceAfterCost = balanceAfterCost, + isBalanceSufficient = isBalanceSufficient, + onConfirm = { + // 确认支付,更新可见性 + viewModel.updateVisibility(pendingIsPrivate) + showVisibilityPaymentDialog = false + }, + onCancel = { + showVisibilityPaymentDialog = false + }, + title = stringResource(R.string.group_chat_info_private_group), + description = stringResource(R.string.group_chat_info_private_group_desc), + isProcessing = viewModel.isUpdatingVisibility + ) + } + // 添加群记忆弹窗 if (showAddMemoryDialog) { ModalBottomSheet( @@ -648,6 +672,10 @@ fun GroupChatInfoScreen(groupId: String) { }, shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp) ) { + // 立即展开到全屏,避免逐渐变高的动画 + LaunchedEffect(Unit) { + memoryManageSheetState.expand() + } GroupMemoryManageContent( groupId = groupId, viewModel = viewModel, @@ -880,8 +908,13 @@ fun AddGroupMemoryDialog( text = "⭐", fontSize = 13.sp ) + val memoryCost = viewModel.addMemoryCost Text( - text = stringResource(R.string.group_chat_info_memory_cost), + text = if (memoryCost != null && memoryCost > 0) { + "添加记忆需消耗 ${memoryCost}派币" + } else { + stringResource(R.string.group_chat_info_memory_cost) + }, style = TextStyle( fontSize = 13.sp, color = Color.Black @@ -980,12 +1013,16 @@ fun AddGroupMemoryDialog( @Composable fun GroupVisibilityDialog( - onDismiss: () -> Unit + viewModel: GroupChatInfoViewModel, + onDismiss: () -> Unit, + onConfirmPrivate: (Boolean) -> Unit ) { val AppColors = LocalAppTheme.current - var isPrivate by remember { mutableStateOf(false) } - val balance = 482 - val unlockCost = 500 + val currentTrtcType = viewModel.groupInfo?.trtcType ?: "Public" + val isPrivateFeePaid = viewModel.groupInfo?.privateFeePaid == true + var isPrivate by remember { mutableStateOf(currentTrtcType == "Private") } + val balance = viewModel.pointsBalance ?: 0 + val unlockCost = viewModel.privateGroupCost ?: 0 Column( modifier = Modifier @@ -1040,14 +1077,15 @@ fun GroupVisibilityDialog( VisibilityOptionItem( title = stringResource(R.string.group_chat_info_private_group), desc = stringResource(R.string.group_chat_info_private_group_desc), - badge = stringResource(R.string.group_chat_info_private_group_cost), + badge = if (!isPrivateFeePaid && unlockCost > 0) "${unlockCost}派币" else null, selected = isPrivate, onClick = { isPrivate = true } ) Spacer(modifier = Modifier.height(16.dp)) - // 余额与费用 + // 余额与费用(仅未解锁时显示) + if (!isPrivateFeePaid) { Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, @@ -1070,6 +1108,7 @@ fun GroupVisibilityDialog( } Spacer(modifier = Modifier.height(8.dp)) + } // 完成按钮 Box( @@ -1086,7 +1125,9 @@ fun GroupVisibilityDialog( ) ) ) - .noRippleClickable { onDismiss() }, + .noRippleClickable { + onConfirmPrivate(isPrivate) + }, contentAlignment = Alignment.Center ) { Text( @@ -1095,12 +1136,15 @@ fun GroupVisibilityDialog( ) } + // 仅未解锁时显示充值提示 + if (!isPrivateFeePaid) { Spacer(modifier = Modifier.height(8.dp)) Text( text = stringResource(R.string.group_chat_info_recharge_hint), style = TextStyle(fontSize = 12.sp, color = Color(0x4D3C3C43)), textAlign = TextAlign.Center ) + } } } diff --git a/app/src/main/java/com/aiosman/ravenow/ui/group/GroupChatInfoViewModel.kt b/app/src/main/java/com/aiosman/ravenow/ui/group/GroupChatInfoViewModel.kt index 79210b5..967393d 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/group/GroupChatInfoViewModel.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/group/GroupChatInfoViewModel.kt @@ -9,15 +9,17 @@ import androidx.lifecycle.viewModelScope import com.aiosman.ravenow.AppStore import com.aiosman.ravenow.ChatState import com.aiosman.ravenow.data.api.ApiClient -import com.aiosman.ravenow.data.api.AgentRule -import com.aiosman.ravenow.data.api.AgentRuleQuota -import com.aiosman.ravenow.data.api.CreateAgentRuleRequestBody -import com.aiosman.ravenow.data.api.UpdateAgentRuleRequestBody +import com.aiosman.ravenow.data.RoomService +import com.aiosman.ravenow.data.RoomServiceImpl import com.aiosman.ravenow.data.parseErrorResponse +import com.aiosman.ravenow.data.PointService +import com.aiosman.ravenow.entity.RoomRuleEntity +import com.aiosman.ravenow.entity.RoomRuleQuotaEntity import com.aiosman.ravenow.entity.ChatNotification import com.aiosman.ravenow.entity.GroupInfo import com.aiosman.ravenow.entity.GroupMember import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch class GroupChatInfoViewModel( @@ -34,30 +36,26 @@ class GroupChatInfoViewModel( val notificationStrategy get() = chatNotification?.strategy ?: "default" // 记忆管理相关状态 - var memoryQuota by mutableStateOf(null) - var memoryList by mutableStateOf>(emptyList()) + var memoryQuota by mutableStateOf(null) + var memoryList by mutableStateOf>(emptyList()) var isLoadingMemory by mutableStateOf(false) var memoryError by mutableStateOf(null) - var promptOpenId by mutableStateOf(null) + + // 房间规则服务 + private val roomService: RoomService = RoomServiceImpl() + + // 群可见性相关状态 + var privateGroupCost by mutableStateOf(null) + var pointsBalance by mutableStateOf(null) + var isLoadingVisibility by mutableStateOf(false) + var isUpdatingVisibility by mutableStateOf(false) + + // 群记忆相关状态 + var addMemoryCost by mutableStateOf(null) init { loadGroupInfo() - loadPromptOpenId() - } - - /** - * 获取群聊中智能体的 OpenID - */ - private fun loadPromptOpenId() { - viewModelScope.launch { - try { - val response = ApiClient.api.createGroupChatAi(trtcGroupId = groupId) - val groupChatResponse = response.body()?.data - val prompts = groupChatResponse?.prompts - promptOpenId = prompts?.firstOrNull()?.openId - } catch (e: Exception) { - Log.e("GroupChatInfoViewModel", "获取智能体OpenID失败: ${e.message}", e) - } - } + loadVisibilityInfo() + loadMemoryCost() } suspend fun updateNotificationStrategy(strategy: String) { val result = ChatState.updateChatNotification(groupId.hashCode(), strategy) @@ -93,7 +91,9 @@ class GroupChatInfoViewModel( "${ApiClient.BASE_API_URL+"/outside"}${it.avatar}"+"?token="+"${AppStore.token}" }, memberCount = room.userCount, - isCreator = room.creator.userId == MyProfileViewModel.profile?.id.toString() + isCreator = room.creator.userId == MyProfileViewModel.profile?.id.toString(), + trtcType = it.trtcType ?: "Public", + privateFeePaid = it.privateFeePaid ?: false ) } @@ -106,57 +106,27 @@ class GroupChatInfoViewModel( } /** - * 添加群记忆 + * 添加群记忆(房间规则) * @param memoryText 记忆内容 - * @param promptOpenId 智能体的 OpenID(可选),如果不提供则从群聊信息中获取 */ - fun addGroupMemory(memoryText: String, promptOpenId: String? = null) { + fun addGroupMemory(memoryText: String) { viewModelScope.launch { try { isAddingMemory = true addMemoryError = null addMemorySuccess = false - // 如果没有提供 promptOpenId,需要先获取群聊的智能体信息 - val openId = promptOpenId ?: run { - // 通过 createGroupChatAi 接口获取群聊详细信息(包含 prompts) - val response = ApiClient.api.createGroupChatAi(trtcGroupId = groupId) - val groupChatResponse = response.body()?.data - val prompts = groupChatResponse?.prompts - - if (prompts.isNullOrEmpty()) { - throw Exception("群聊中没有找到智能体,无法添加记忆") - } - - // 使用第一个智能体的 openId - prompts.firstOrNull()?.openId - ?: throw Exception("无法获取智能体信息") - } - - if (openId.isBlank()) { - throw Exception("智能体ID不能为空") - } - - // 创建智能体规则(群记忆) - val requestBody = CreateAgentRuleRequestBody( + // 使用房间规则接口创建群记忆 + roomService.createRoomRule( rule = memoryText, - openId = openId + trtcId = groupId ) - val response = ApiClient.api.createAgentRule(requestBody) - - if (response.isSuccessful) { - addMemorySuccess = true - Log.d("GroupChatInfoViewModel", "群记忆添加成功") - // 刷新记忆列表和配额 - loadMemoryQuota(openId) - loadMemoryList(openId) - } else { - val errorResponse = parseErrorResponse(response.errorBody()) - val errorMessage = errorResponse?.toServiceException()?.message - ?: "添加群记忆失败: ${response.code()}" - throw Exception(errorMessage) - } + addMemorySuccess = true + Log.d("GroupChatInfoViewModel", "群记忆添加成功") + // 刷新记忆列表和配额 + loadMemoryQuota() + loadMemoryList() } catch (e: Exception) { addMemoryError = e.message ?: "添加群记忆失败" Log.e("GroupChatInfoViewModel", "添加群记忆失败: ${e.message}", e) @@ -167,38 +137,16 @@ class GroupChatInfoViewModel( } /** - * 获取记忆配额信息 + * 获取记忆配额信息(房间规则配额) */ - fun loadMemoryQuota(openId: String? = null) { + fun loadMemoryQuota() { viewModelScope.launch { try { isLoadingMemory = true memoryError = null - val targetOpenId = openId ?: promptOpenId - if (targetOpenId.isNullOrBlank()) { - // 如果还没有获取到 openId,先获取 - val response = ApiClient.api.createGroupChatAi(trtcGroupId = groupId) - val groupChatResponse = response.body()?.data - val prompts = groupChatResponse?.prompts - val fetchedOpenId = prompts?.firstOrNull()?.openId - ?: throw Exception("无法获取智能体信息") - - promptOpenId = fetchedOpenId - val quotaResponse = ApiClient.api.getAgentRuleQuota(fetchedOpenId) - if (quotaResponse.isSuccessful) { - memoryQuota = quotaResponse.body()?.data - } else { - throw Exception("获取配额信息失败: ${quotaResponse.code()}") - } - } else { - val quotaResponse = ApiClient.api.getAgentRuleQuota(targetOpenId) - if (quotaResponse.isSuccessful) { - memoryQuota = quotaResponse.body()?.data - } else { - throw Exception("获取配额信息失败: ${quotaResponse.code()}") - } - } + // 使用房间规则接口获取配额 + memoryQuota = roomService.getRoomRuleQuota(trtcId = groupId) } catch (e: Exception) { memoryError = e.message ?: "获取配额信息失败" Log.e("GroupChatInfoViewModel", "获取配额信息失败: ${e.message}", e) @@ -209,38 +157,21 @@ class GroupChatInfoViewModel( } /** - * 获取记忆列表 + * 获取记忆列表(房间规则列表) */ - fun loadMemoryList(openId: String? = null, page: Int = 1, pageSize: Int = 20) { + fun loadMemoryList(page: Int = 1, pageSize: Int = 20) { viewModelScope.launch { try { isLoadingMemory = true memoryError = null - val targetOpenId = openId ?: promptOpenId - if (targetOpenId.isNullOrBlank()) { - // 如果还没有获取到 openId,先获取 - val response = ApiClient.api.createGroupChatAi(trtcGroupId = groupId) - val groupChatResponse = response.body()?.data - val prompts = groupChatResponse?.prompts - val fetchedOpenId = prompts?.firstOrNull()?.openId - ?: throw Exception("无法获取智能体信息") - - promptOpenId = fetchedOpenId - val listResponse = ApiClient.api.getAgentRuleList(fetchedOpenId, page = page, pageSize = pageSize) - if (listResponse.isSuccessful) { - memoryList = listResponse.body()?.data?.list ?: emptyList() - } else { - throw Exception("获取记忆列表失败: ${listResponse.code()}") - } - } else { - val listResponse = ApiClient.api.getAgentRuleList(targetOpenId, page = page, pageSize = pageSize) - if (listResponse.isSuccessful) { - memoryList = listResponse.body()?.data?.list ?: emptyList() - } else { - throw Exception("获取记忆列表失败: ${listResponse.code()}") - } - } + // 使用房间规则接口获取列表 + val result = roomService.getRoomRuleList( + trtcId = groupId, + page = page, + pageSize = pageSize + ) + memoryList = result.list } catch (e: Exception) { memoryError = e.message ?: "获取记忆列表失败" Log.e("GroupChatInfoViewModel", "获取记忆列表失败: ${e.message}", e) @@ -251,7 +182,7 @@ class GroupChatInfoViewModel( } /** - * 删除记忆 + * 删除记忆(房间规则) */ fun deleteMemory(ruleId: Int) { viewModelScope.launch { @@ -259,19 +190,12 @@ class GroupChatInfoViewModel( isLoadingMemory = true memoryError = null - val response = ApiClient.api.deleteAgentRule(ruleId) - if (response.isSuccessful) { - // 刷新记忆列表和配额 - promptOpenId?.let { openId -> - loadMemoryQuota(openId) - loadMemoryList(openId) - } - } else { - val errorResponse = parseErrorResponse(response.errorBody()) - val errorMessage = errorResponse?.toServiceException()?.message - ?: "删除记忆失败: ${response.code()}" - throw Exception(errorMessage) - } + // 使用房间规则接口删除 + roomService.deleteRoomRule(ruleId) + + // 刷新记忆列表和配额 + loadMemoryQuota() + loadMemoryList() } catch (e: Exception) { memoryError = e.message ?: "删除记忆失败" Log.e("GroupChatInfoViewModel", "删除记忆失败: ${e.message}", e) @@ -282,34 +206,23 @@ class GroupChatInfoViewModel( } /** - * 更新记忆 + * 更新记忆(房间规则) */ - fun updateMemory(ruleId: Int, newRuleText: String, targetOpenId: String? = null) { + fun updateMemory(ruleId: Int, newRuleText: String) { viewModelScope.launch { try { isLoadingMemory = true memoryError = null - val openId = targetOpenId ?: promptOpenId - ?: throw Exception("无法获取智能体ID") - - val requestBody = UpdateAgentRuleRequestBody( + // 使用房间规则接口更新 + roomService.updateRoomRule( id = ruleId, - rule = newRuleText, - openId = openId + rule = newRuleText ) - val response = ApiClient.api.updateAgentRule(requestBody) - if (response.isSuccessful) { - // 刷新记忆列表和配额 - loadMemoryQuota(openId) - loadMemoryList(openId) - } else { - val errorResponse = parseErrorResponse(response.errorBody()) - val errorMessage = errorResponse?.toServiceException()?.message - ?: "更新记忆失败: ${response.code()}" - throw Exception(errorMessage) - } + // 刷新记忆列表和配额 + loadMemoryQuota() + loadMemoryList() } catch (e: Exception) { memoryError = e.message ?: "更新记忆失败" Log.e("GroupChatInfoViewModel", "更新记忆失败: ${e.message}", e) @@ -318,4 +231,77 @@ class GroupChatInfoViewModel( } } } + + /** + * 加载群可见性相关信息(价格和积分余额) + */ + fun loadVisibilityInfo() { + viewModelScope.launch { + try { + isLoadingVisibility = true + + // 获取积分规则中的私密群组价格 + PointService.refreshPointsRules() + val rules = PointService.pointsRules.first() + val roomPrivateRule = rules?.sub?.get(PointService.PointsRuleKey.ROOM_PRIVATE) + privateGroupCost = when (roomPrivateRule) { + is PointService.RuleAmount.Fixed -> roomPrivateRule.value + is PointService.RuleAmount.Range -> roomPrivateRule.min + null -> null + } + + // 获取积分余额 + PointService.refreshMyPointsBalance(includeStatistics = false) + val balance = PointService.pointsBalance.first() + pointsBalance = balance?.balance + } catch (e: Exception) { + Log.e("GroupChatInfoViewModel", "加载可见性信息失败: ${e.message}", e) + } finally { + isLoadingVisibility = false + } + } + } + + /** + * 更新群可见性 + * @param isPrivate 是否设置为私密群组 + */ + fun updateVisibility(isPrivate: Boolean) { + viewModelScope.launch { + try { + isUpdatingVisibility = true + // TODO: 实现更新房间可见性的接口调用 + // 暂时留空 + + // 更新成功后刷新群信息和积分余额 + loadGroupInfo() + loadVisibilityInfo() + } catch (e: Exception) { + Log.e("GroupChatInfoViewModel", "更新可见性失败: ${e.message}", e) + } finally { + isUpdatingVisibility = false + } + } + } + + /** + * 加载添加群记忆的价格 + */ + fun loadMemoryCost() { + viewModelScope.launch { + try { + // 获取积分规则中的房间记忆价格(群聊记忆,区别于 Agent 记忆) + PointService.refreshPointsRules() + val rules = PointService.pointsRules.first() + val roomMemoryRule = rules?.sub?.get(PointService.PointsRuleKey.SPEND_ROOM_MEMORY) + addMemoryCost = when (roomMemoryRule) { + is PointService.RuleAmount.Fixed -> roomMemoryRule.value + is PointService.RuleAmount.Range -> roomMemoryRule.min + else -> null + } + } catch (e: Exception) { + Log.e("GroupChatInfoViewModel", "加载记忆价格失败: ${e.message}", e) + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/ravenow/ui/group/GroupMemoryManageScreen.kt b/app/src/main/java/com/aiosman/ravenow/ui/group/GroupMemoryManageScreen.kt index 3c19423..b35de50 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/group/GroupMemoryManageScreen.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/group/GroupMemoryManageScreen.kt @@ -39,6 +39,8 @@ import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.platform.LocalContext import android.widget.Toast import androidx.compose.ui.graphics.Brush +import com.aiosman.ravenow.ui.composables.CustomAsyncImage +import androidx.compose.ui.layout.ContentScale @Composable fun GroupMemoryManageContent( @@ -48,9 +50,6 @@ fun GroupMemoryManageContent( onDismiss: () -> Unit = {} ) { val AppColors = LocalAppTheme.current - val configuration = LocalConfiguration.current - val screenHeight = configuration.screenHeightDp.dp - val sheetHeight = screenHeight * 0.95f val context = LocalContext.current // 编辑记忆的状态 - 存储正在编辑的记忆ID @@ -68,8 +67,7 @@ fun GroupMemoryManageContent( Column( modifier = Modifier - .fillMaxWidth() - .height(sheetHeight) + .fillMaxSize() .background(Color(0xFFFAF9FB)) ) { // 顶部栏:返回按钮 + 标题 + 加号按钮 @@ -257,7 +255,7 @@ fun GroupMemoryManageContent( @OptIn(ExperimentalMaterial3Api::class) @Composable fun EditGroupMemoryDialog( - memory: com.aiosman.ravenow.data.api.AgentRule, + memory: com.aiosman.ravenow.entity.RoomRuleEntity, viewModel: GroupChatInfoViewModel, onDismiss: () -> Unit, onUpdateMemory: (String) -> Unit @@ -403,7 +401,7 @@ fun EditGroupMemoryDialog( */ @Composable fun MemoryItem( - memory: com.aiosman.ravenow.data.api.AgentRule, + memory: com.aiosman.ravenow.entity.RoomRuleEntity, isEditing: Boolean = false, onEdit: () -> Unit = {}, onCancel: () -> Unit = {}, @@ -579,20 +577,44 @@ fun MemoryItem( Spacer(modifier = Modifier.height(16.dp)) - // 底部行:日期 + 编辑删除按钮 + // 底部行:创建者信息 + 编辑删除按钮 Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.Bottom + verticalAlignment = Alignment.CenterVertically ) { - // 日期文本 - 左侧 - Text( - text = formattedDate, - style = TextStyle( - color = Color(0x993C3C43), - fontSize = 11.sp + // 创建者信息 - 左侧:头像 + 用户名 · 时间 + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // 圆形头像 + val avatarUrl = memory.creator?.avatarDirectUrl?.takeIf { it.isNotBlank() } + CustomAsyncImage( + imageUrl = avatarUrl, + contentDescription = "创建者头像", + modifier = Modifier + .size(20.dp) + .clip(CircleShape), + defaultRes = R.drawable.default_avatar, + placeholderRes = R.drawable.default_avatar, + errorRes = R.drawable.default_avatar, + contentScale = ContentScale.Crop ) - ) + + // 用户名 · 时间 + Text( + text = buildString { + append(memory.creator?.nickname ?: "未知用户") + append(" · ") + append(formattedDate) + }, + style = TextStyle( + color = Color(0x993C3C43), + fontSize = 11.sp + ) + ) + } // 编辑和删除图标 - 右侧 Row( diff --git a/app/src/main/java/com/aiosman/ravenow/ui/profile/AgentMemoryManageScreen.kt b/app/src/main/java/com/aiosman/ravenow/ui/profile/AgentMemoryManageScreen.kt new file mode 100644 index 0000000..b89745a --- /dev/null +++ b/app/src/main/java/com/aiosman/ravenow/ui/profile/AgentMemoryManageScreen.kt @@ -0,0 +1,806 @@ +package com.aiosman.ravenow.ui.profile + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +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.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import android.widget.Toast +import androidx.compose.foundation.Image +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.rememberModalBottomSheetState +import com.aiosman.ravenow.LocalAppTheme +import com.aiosman.ravenow.R +import com.aiosman.ravenow.data.AgentRuleEntity +import com.aiosman.ravenow.entity.AccountProfileEntity +import com.aiosman.ravenow.ui.composables.CustomAsyncImage +import com.aiosman.ravenow.ui.modifiers.noRippleClickable + +@Composable +fun AgentMemoryManageContent( + openId: String, + profile: AccountProfileEntity?, + viewModel: AgentMemoryManageViewModel, + onAddMemoryClick: () -> Unit = {}, + onDismiss: () -> Unit = {} +) { + val AppColors = LocalAppTheme.current + val context = LocalContext.current + + // 编辑记忆的状态 - 存储正在编辑的记忆ID + var editingMemoryId by remember { mutableStateOf(null) } + + // 加载配额和列表数据 + LaunchedEffect(Unit) { + viewModel.loadMemoryQuota() + viewModel.loadMemoryList() + } + + val quota = viewModel.memoryQuota + val memoryList = viewModel.memoryList + val isLoading = viewModel.isLoadingMemory + + Column( + modifier = Modifier + .fillMaxSize() + .background(Color(0xFFFAF9FB)) + ) { + // 顶部栏:返回按钮 + 标题 + 加号按钮 + Box( + modifier = Modifier + .fillMaxWidth() + .height(44.dp) + .padding(horizontal = 16.dp) + ) { + // 中间标题 - 绝对居中,不受其他组件影响 + Text( + text = stringResource(R.string.group_chat_info_memory_manage), + style = TextStyle( + color = Color.Black, + fontSize = 17.sp, + fontWeight = FontWeight.SemiBold + ), + modifier = Modifier.align(Alignment.Center), + textAlign = TextAlign.Center + ) + + // 左侧返回按钮 + Row( + modifier = Modifier + .align(Alignment.CenterStart) + .clip(RoundedCornerShape(296.dp)) + .background(Color.White) + .noRippleClickable { onDismiss() } + .padding(horizontal = 12.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Image( + painter = painterResource(R.drawable.rider_pro_back_icon), + contentDescription = "返回", + modifier = Modifier.size(16.dp), + colorFilter = ColorFilter.tint(Color.Black) + ) + Text( + text = "返回", + style = TextStyle( + color = Color.Black, + fontSize = 15.sp, + fontWeight = FontWeight.Normal + ) + ) + } + + // 右侧圆形加号按钮 + Box( + modifier = Modifier + .align(Alignment.CenterEnd) + .size(32.dp) + .clip(CircleShape) + .background(Color.White) + .noRippleClickable { onAddMemoryClick() }, + contentAlignment = Alignment.Center + ) { + Text( + text = "+", + style = TextStyle( + color = Color.Black, + fontSize = 20.sp, + fontWeight = FontWeight.Medium + ) + ) + } + } + + // 浅黄色提示栏 - 显示真实的配额数据 + Row( + modifier = Modifier + .fillMaxWidth() + .background(Color(0xFFFBF8EF)) + .padding(horizontal = 16.dp, vertical = 12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text("已付费:", style = TextStyle(color = Color(0x993C3C43), fontSize = 13.sp)) + Spacer(Modifier.width(3.dp)) + Text( + "${quota?.purchasedCount ?: 0}", + style = TextStyle(color = Color(0xFFFF8D28), fontSize = 13.sp) + ) + } + Row(verticalAlignment = Alignment.CenterVertically) { + Text("已使用:", style = TextStyle(color = Color(0x993C3C43), fontSize = 13.sp)) + Spacer(Modifier.width(3.dp)) + Text( + "${quota?.currentCount ?: 0}", + style = TextStyle(color = Color(0xFFFF8D28), fontSize = 13.sp) + ) + } + } + Row(verticalAlignment = Alignment.CenterVertically) { + Text("可用上限:", style = TextStyle(color = Color(0x993C3C43), fontSize = 13.sp)) + Spacer(Modifier.width(3.dp)) + Text( + "${quota?.totalMaxCount ?: 50}", + style = TextStyle(color = Color(0xFFFF8D28), fontSize = 13.sp) + ) + } + } + + // 记忆列表或空状态 + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) { + if (isLoading) { + // 加载中状态 + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + modifier = Modifier.size(40.dp), + color = Color(0xFFFF8D28) + ) + } + } else if (memoryList.isNotEmpty()) { + // 显示记忆列表 + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + items(memoryList) { memory -> + AgentMemoryItem( + memory = memory, + isEditing = editingMemoryId == memory.id, + onEdit = { + editingMemoryId = memory.id + }, + onCancel = { + editingMemoryId = null + }, + onSave = { newText -> + viewModel.updateMemory(memory.id, newText) + editingMemoryId = null + }, + onDelete = { + viewModel.deleteMemory(memory.id) + } + ) + } + } + } else { + Column( + modifier = Modifier + .fillMaxSize() + .padding(vertical = 60.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Image( + painter = painterResource(id = R.mipmap.group), + contentDescription = "暂无记忆", + modifier = Modifier + .height(150.dp).width(180.dp) + ) + + Spacer(Modifier.height(10.dp)) + Text( + text = "暂无记忆", + style = TextStyle(color = Color.Black, fontSize = 16.sp, fontWeight = FontWeight.SemiBold) + ) + Spacer(Modifier.height(6.dp)) + Text( + text = "点击上方按钮添加 Agent 记忆", + style = TextStyle(color = Color.Black, fontSize = 14.sp, fontWeight = FontWeight.Normal) + ) + } + } + } + } +} + +/** + * Agent 记忆项组件(不显示创建者信息) + */ +@Composable +fun AgentMemoryItem( + memory: AgentRuleEntity, + isEditing: Boolean = false, + onEdit: () -> Unit = {}, + onCancel: () -> Unit = {}, + onSave: (String) -> Unit = {}, + onDelete: () -> Unit +) { + val AppColors = LocalAppTheme.current + val context = LocalContext.current + var memoryText by remember { mutableStateOf(memory.rule) } + val maxLength = 500 + + // 渐变边框颜色 + val gradientColors = listOf( + Color(0xFF7C45ED), + Color(0xFF7C57EE), + Color(0xFF7BD8F8) + ) + val gradientBrush = Brush.horizontalGradient(colors = gradientColors) + + // 当进入编辑模式时,重置文本 + LaunchedEffect(isEditing) { + if (isEditing) { + memoryText = memory.rule + } + } + + if (isEditing) { + // 编辑模式:显示编辑界面 + Column( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .background(Color.White) + .padding(horizontal = 12.dp, vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + // 文本输入框 - 带渐变边框 + Box( + modifier = Modifier + .fillMaxWidth() + .height(88.dp) + ) { + // 渐变边框层 + Box( + modifier = Modifier + .fillMaxSize() + .clip(RoundedCornerShape(16.dp)) + .background(brush = gradientBrush) + ) + + // 内容层 - 白色背景,通过padding形成边框效果 + Box( + modifier = Modifier + .fillMaxSize() + .padding(1.dp) + .clip(RoundedCornerShape(15.dp)) + .background(Color.White) + .padding(12.dp), + contentAlignment = Alignment.TopStart + ) { + BasicTextField( + value = memoryText, + onValueChange = { newText -> + if (newText.length <= maxLength) { + memoryText = newText + } + }, + cursorBrush = SolidColor(Color.Black), + modifier = Modifier.fillMaxWidth(), + textStyle = TextStyle( + fontSize = 13.sp, + color = Color.Black, + lineHeight = 18.sp + ), + decorationBox = { innerTextField -> + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.TopStart + ) { + innerTextField() + } + } + ) + } + } + + // 按钮行:取消和保存 + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + // 取消按钮 + Box( + modifier = Modifier + .weight(1f) + .height(34.dp) + .clip(RoundedCornerShape(653.8.dp)) + .background(Color(0x147C7480)) + .noRippleClickable { onCancel() }, + contentAlignment = Alignment.Center + ) { + Text( + text = "取消", + style = TextStyle( + fontSize = 15.sp, + color = Color.Black + ) + ) + } + + // 保存按钮 + Box( + modifier = Modifier + .weight(1f) + .height(34.dp) + .clip(RoundedCornerShape(653.8.dp)) + .background(Color(0xFF110C13)) + .noRippleClickable { + if (memoryText.isNotBlank() && memoryText != memory.rule) { + onSave(memoryText) + Toast.makeText(context, "记忆更新成功", Toast.LENGTH_SHORT).show() + } + }, + contentAlignment = Alignment.Center + ) { + Text( + text = "保存", + style = TextStyle( + fontSize = 15.sp, + color = Color.White + ) + ) + } + } + } + } else { + // 显示模式:显示记忆内容(不显示创建者信息) + // 格式化日期:从 "2025-10-20T10:30:00Z" 格式转换为 "2025年10月20日" + val formattedDate = try { + if (memory.createdAt.length >= 10) { + val dateStr = memory.createdAt.substring(0, 10) + val parts = dateStr.split("-") + if (parts.size == 3) { + "${parts[0]}年${parts[1].toInt()}月${parts[2].toInt()}日" + } else { + dateStr + } + } else { + memory.createdAt + } + } catch (e: Exception) { + if (memory.createdAt.length >= 10) memory.createdAt.substring(0, 10) else memory.createdAt + } + + Column( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .background(Color.White) + .padding(horizontal = 12.dp, vertical = 16.dp) + ) { + // 主文本 - 顶部 + Text( + text = memory.rule, + style = TextStyle( + color = Color.Black, + fontSize = 13.sp, + lineHeight = 18.sp + ), + maxLines = 3, + overflow = TextOverflow.Ellipsis + ) + + Spacer(modifier = Modifier.height(16.dp)) + + // 底部行:时间 + 编辑删除按钮(不显示创建者信息) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + // 时间信息 - 左侧 + Text( + text = formattedDate, + style = TextStyle( + color = Color(0x993C3C43), + fontSize = 11.sp + ) + ) + + // 编辑和删除图标 - 右侧 + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // 编辑图标 + Image( + painter = painterResource(R.mipmap.icons_infor_edit), + contentDescription = "编辑", + modifier = Modifier + .size(20.dp) + .noRippleClickable { onEdit() }, + colorFilter = ColorFilter.tint(Color.Black) + ) + + Image( + painter = painterResource(R.mipmap.iconsdelete), + contentDescription = "删除", + modifier = Modifier + .size(20.dp) + .noRippleClickable { onDelete() }, + colorFilter = ColorFilter.tint(Color(0xFFEE2A33)) + ) + } + } + } + } +} + +/** + * 添加 Agent 记忆对话框 + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AddAgentMemoryDialog( + profile: AccountProfileEntity?, + viewModel: AgentMemoryManageViewModel, + onDismiss: () -> Unit, + onRequestAddMemory: (String) -> Unit // 改为请求添加,而不是直接添加 +) { + val AppColors = LocalAppTheme.current + val context = LocalContext.current + var memoryText by remember { mutableStateOf("") } + val maxLength = 500 + + // 监听添加记忆的结果 + LaunchedEffect(viewModel.addMemorySuccess) { + if (viewModel.addMemorySuccess) { + Toast.makeText(context, context.getString(R.string.group_chat_info_memory_add_success), Toast.LENGTH_SHORT).show() + memoryText = "" // 清空输入框 + onDismiss() + viewModel.addMemorySuccess = false + } + } + + LaunchedEffect(viewModel.addMemoryError) { + viewModel.addMemoryError?.let { error -> + Toast.makeText(context, error, Toast.LENGTH_SHORT).show() + viewModel.addMemoryError = null + } + } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(bottom = 40.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // 顶部标题栏 + Row( + modifier = Modifier + .fillMaxWidth() + .height(44.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Spacer(modifier = Modifier.width(24.dp)) + Text( + text = stringResource(R.string.group_chat_info_add_memory), + style = TextStyle( + fontSize = 17.sp, + fontWeight = FontWeight.SemiBold, + color = Color.Black + ), + modifier = Modifier.weight(1f), + textAlign = TextAlign.Center + ) + Image( + painter = painterResource(R.drawable.rider_pro_close), + contentDescription = "关闭", + modifier = Modifier + .size(24.dp) + .noRippleClickable { onDismiss() }, + colorFilter = ColorFilter.tint(Color.Black) + ) + } + + Spacer(modifier = Modifier.height(10.dp)) + + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + // Agent 信息卡片 + Row( + modifier = Modifier + .fillMaxWidth() + .height(80.dp) + .clip(RoundedCornerShape(16.dp)) + .background(Color.White) + .padding(horizontal = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + if (profile?.avatar?.isNotEmpty() == true) { + CustomAsyncImage( + imageUrl = profile.avatar, + modifier = Modifier + .size(48.dp) + .clip(RoundedCornerShape(12.dp)), + contentDescription = "Agent 头像", + context = LocalContext.current + ) + } else { + Box( + modifier = Modifier + .size(48.dp) + .clip(RoundedCornerShape(12.dp)) + .background(AppColors.decentBackground), + contentAlignment = Alignment.Center + ) { + Text( + text = profile?.nickName?.firstOrNull()?.toString() ?: "", + style = TextStyle( + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + color = AppColors.text + ) + ) + } + } + + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = profile?.nickName ?: "", + style = TextStyle( + fontSize = 15.sp, + fontWeight = FontWeight.SemiBold, + color = Color.Black + ) + ) + if (!profile?.bio.isNullOrEmpty()) { + Text( + text = profile?.bio ?: "", + style = TextStyle( + fontSize = 12.sp, + color = Color(0xFF3C3C43).copy(alpha = 0.6f) + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + } + + // 输入框卡片 + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .background(Color.White) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp) + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(84.dp), + contentAlignment = Alignment.TopStart + ) { + BasicTextField( + value = memoryText, + onValueChange = { newText -> + if (newText.length <= maxLength) { + memoryText = newText + } + }, + cursorBrush = SolidColor(Color.Black), + modifier = Modifier.fillMaxWidth(), + maxLines = 6, + textStyle = TextStyle( + fontSize = 15.sp, + color = Color.Black, + lineHeight = 20.sp + ), + decorationBox = { innerTextField -> + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.TopStart + ) { + innerTextField() + if (memoryText.isEmpty()) { + Text( + text = stringResource(R.string.group_chat_info_memory_input_hint), + style = TextStyle( + fontSize = 15.sp, + color = Color.Black.copy(alpha = 0.3f) + ) + ) + } + } + } + ) + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { + Text( + text = "${memoryText.length}/$maxLength", + style = TextStyle( + fontSize = 12.sp, + color = Color(0xFF3C3C43).copy(alpha = 0.3f) + ) + ) + } + } + } + + // 提示信息卡片 + Column( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .background(Color(0xFFFBF8EF)) + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = "⭐", + fontSize = 13.sp + ) + val memoryCost = viewModel.addMemoryCost + Text( + text = if (memoryCost != null && memoryCost > 0) { + "添加记忆需消耗 ${memoryCost}派币" + } else { + stringResource(R.string.group_chat_info_memory_cost) + }, + style = TextStyle( + fontSize = 13.sp, + color = Color.Black + ) + ) + } + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = "🤖", + fontSize = 13.sp + ) + Text( + text = stringResource(R.string.group_chat_info_memory_optimization), + style = TextStyle( + fontSize = 13.sp, + color = Color.Black + ) + ) + } + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = "✏️", + fontSize = 13.sp + ) + Text( + text = stringResource(R.string.group_chat_info_memory_editable), + style = TextStyle( + fontSize = 13.sp, + color = Color.Black + ) + ) + } + } + + // 添加记忆按钮 + val isButtonEnabled = memoryText.isNotEmpty() && !viewModel.isAddingMemory + Box( + modifier = Modifier + .fillMaxWidth() + .height(50.dp) + .clip(RoundedCornerShape(1000.dp)) + .then( + if (isButtonEnabled) { + Modifier.background( + brush = Brush.horizontalGradient( + colors = listOf( + Color(0xFF7C45ED), + Color(0x997C57EE), + Color(0x887BD8F8) + ) + ) + ) + } else { + Modifier.background( + brush = Brush.horizontalGradient( + colors = listOf( + Color(0x337C45ED), + Color(0x337C57EE), + Color(0x337BD8F8) + ) + ) + ) + } + ) + .noRippleClickable { + if (isButtonEnabled) { + onRequestAddMemory(memoryText) + } + }, + contentAlignment = Alignment.Center + ) { + if (viewModel.isAddingMemory) { + CircularProgressIndicator( + modifier = Modifier.size(20.dp), + color = Color.White + ) + } else { + Text( + text = stringResource(R.string.group_chat_info_add_memory), + style = TextStyle( + fontSize = 17.sp, + color = if (isButtonEnabled) Color.White else Color.White.copy(alpha = 0.6f) + ) + ) + } + } + } + } +} + diff --git a/app/src/main/java/com/aiosman/ravenow/ui/profile/AgentMemoryManageViewModel.kt b/app/src/main/java/com/aiosman/ravenow/ui/profile/AgentMemoryManageViewModel.kt new file mode 100644 index 0000000..dbabac8 --- /dev/null +++ b/app/src/main/java/com/aiosman/ravenow/ui/profile/AgentMemoryManageViewModel.kt @@ -0,0 +1,207 @@ +package com.aiosman.ravenow.ui.profile + +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.AgentRuleService +import com.aiosman.ravenow.data.AgentRuleServiceImpl +import com.aiosman.ravenow.data.AgentRuleEntity +import com.aiosman.ravenow.data.AgentRuleQuotaEntity +import com.aiosman.ravenow.data.PointService +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch + +class AgentMemoryManageViewModel( + private val openId: String +) : ViewModel() { + + // 记忆管理相关状态 + var memoryQuota by mutableStateOf(null) + var memoryList by mutableStateOf>(emptyList()) + var isLoadingMemory by mutableStateOf(false) + var memoryError by mutableStateOf(null) + + // Agent 规则服务 + private val agentRuleService: AgentRuleService = AgentRuleServiceImpl() + + // 添加记忆相关状态 + var isAddingMemory by mutableStateOf(false) + var addMemoryError by mutableStateOf(null) + var addMemorySuccess by mutableStateOf(false) + var addMemoryCost by mutableStateOf(null) + var pointsBalance by mutableStateOf(null) + + init { + loadMemoryCost() + loadPointsBalance() + } + + /** + * 加载积分余额 + */ + fun loadPointsBalance() { + viewModelScope.launch { + try { + PointService.refreshMyPointsBalance(includeStatistics = false) + val balance = PointService.pointsBalance.first() + pointsBalance = balance?.balance + } catch (e: Exception) { + Log.e("AgentMemoryManageViewModel", "加载积分余额失败: ${e.message}", e) + } + } + } + + /** + * 添加 Agent 记忆 + * @param memoryText 记忆内容 + */ + fun addAgentMemory(memoryText: String) { + viewModelScope.launch { + try { + isAddingMemory = true + addMemoryError = null + addMemorySuccess = false + + // 使用 Agent 规则接口创建记忆 + agentRuleService.createAgentRuleByOpenId( + openId = openId, + rule = memoryText + ) + + addMemorySuccess = true + Log.d("AgentMemoryManageViewModel", "Agent 记忆添加成功") + // 刷新记忆列表和配额 + loadMemoryQuota() + loadMemoryList() + } catch (e: Exception) { + addMemoryError = e.message ?: "添加 Agent 记忆失败" + Log.e("AgentMemoryManageViewModel", "添加 Agent 记忆失败: ${e.message}", e) + } finally { + isAddingMemory = false + } + } + } + + /** + * 获取记忆配额信息(Agent 规则配额) + */ + fun loadMemoryQuota() { + viewModelScope.launch { + try { + isLoadingMemory = true + memoryError = null + + // 使用 Agent 规则接口获取配额 + memoryQuota = agentRuleService.getAgentRuleQuota(openId = openId) + } catch (e: Exception) { + memoryError = e.message ?: "获取配额信息失败" + Log.e("AgentMemoryManageViewModel", "获取配额信息失败: ${e.message}", e) + } finally { + isLoadingMemory = false + } + } + } + + /** + * 获取记忆列表(Agent 规则列表) + */ + fun loadMemoryList(page: Int = 1, pageSize: Int = 20) { + viewModelScope.launch { + try { + isLoadingMemory = true + memoryError = null + + // 使用 Agent 规则接口获取列表 + val result = agentRuleService.getAgentRuleList( + openId = openId, + keyword = null, + page = page, + pageSize = pageSize + ) + memoryList = result.list + } catch (e: Exception) { + memoryError = e.message ?: "获取记忆列表失败" + Log.e("AgentMemoryManageViewModel", "获取记忆列表失败: ${e.message}", e) + } finally { + isLoadingMemory = false + } + } + } + + /** + * 删除记忆(Agent 规则) + */ + fun deleteMemory(ruleId: Int) { + viewModelScope.launch { + try { + isLoadingMemory = true + memoryError = null + + // 使用 Agent 规则接口删除 + agentRuleService.deleteAgentRule(ruleId) + + // 刷新记忆列表和配额 + loadMemoryQuota() + loadMemoryList() + } catch (e: Exception) { + memoryError = e.message ?: "删除记忆失败" + Log.e("AgentMemoryManageViewModel", "删除记忆失败: ${e.message}", e) + } finally { + isLoadingMemory = false + } + } + } + + /** + * 更新记忆(Agent 规则) + */ + fun updateMemory(ruleId: Int, newRuleText: String) { + viewModelScope.launch { + try { + isLoadingMemory = true + memoryError = null + + // 使用 Agent 规则接口更新 + agentRuleService.updateAgentRule( + id = ruleId, + rule = newRuleText, + openId = openId + ) + + // 刷新记忆列表和配额 + loadMemoryQuota() + loadMemoryList() + } catch (e: Exception) { + memoryError = e.message ?: "更新记忆失败" + Log.e("AgentMemoryManageViewModel", "更新记忆失败: ${e.message}", e) + } finally { + isLoadingMemory = false + } + } + } + + /** + * 加载添加 Agent 记忆的价格 + */ + fun loadMemoryCost() { + viewModelScope.launch { + try { + // 获取积分规则中的 Agent 记忆价格 + PointService.refreshPointsRules() + val rules = PointService.pointsRules.first() + val agentMemoryRule = rules?.sub?.get(PointService.PointsRuleKey.ADD_AGENT_MEMORY) + addMemoryCost = when (agentMemoryRule) { + is PointService.RuleAmount.Fixed -> agentMemoryRule.value + is PointService.RuleAmount.Range -> agentMemoryRule.min + else -> null + } + } catch (e: Exception) { + Log.e("AgentMemoryManageViewModel", "加载记忆价格失败: ${e.message}", e) + } + } + } +} + diff --git a/app/src/main/java/com/aiosman/ravenow/ui/profile/AiProfileV3.kt b/app/src/main/java/com/aiosman/ravenow/ui/profile/AiProfileV3.kt index 1e0e2a5..307a594 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/profile/AiProfileV3.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/profile/AiProfileV3.kt @@ -57,6 +57,7 @@ fun AiProfileV3( onFollowClick: () -> Unit = {}, onChatClick: () -> Unit = {}, onShareClick: () -> Unit = {}, + onMemoryManageClick: () -> Unit = {}, onLoadMore: () -> Unit = {}, onComment: (MomentEntity) -> Unit = {}, ) { @@ -160,7 +161,7 @@ fun AiProfileV3( ) } - // 底部菜单 + // 底部菜单 if (showMenu) { AiProfileMenuModal( onDismiss = { showMenu = false }, @@ -181,6 +182,10 @@ fun AiProfileV3( ) } }, + onMemoryManageClick = { + showMenu = false + onMemoryManageClick() + }, profile = profile, showEdit = isCreator ) @@ -525,6 +530,7 @@ private fun AiProfileMenuModal( onChatClick: () -> Unit, onShareClick: () -> Unit, onEditClick: () -> Unit, + onMemoryManageClick: () -> Unit, showEdit: Boolean = false, profile: AccountProfileEntity? = null ) { @@ -541,7 +547,7 @@ private fun AiProfileMenuModal( Row( modifier = Modifier .fillMaxWidth() - .height(if (showEdit) 240.dp else 160.dp) + .height(if (showEdit) 240.dp else 240.dp) .background(appColors.background) .padding(vertical = 47.dp, horizontal = 20.dp) ) { @@ -581,7 +587,7 @@ private fun AiProfileMenuModal( Column( modifier = Modifier .weight(1f) - .padding(end = if (showEdit) 16.dp else 0.dp), + .padding(end = 16.dp), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { @@ -609,6 +615,38 @@ private fun AiProfileMenuModal( ) } + // 记忆管理选项 + 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 { + onMemoryManageClick() + } + ) { + Image( + painter = painterResource(id = R.drawable.ic_brain_add), + contentDescription = "", + modifier = Modifier.size(24.dp), + colorFilter = ColorFilter.tint(appColors.text) + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = stringResource(R.string.group_chat_info_memory_manage), + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + color = appColors.text + ) + } + // 编辑选项(仅创建者可见) if (showEdit) { Column( diff --git a/app/src/main/java/com/aiosman/ravenow/ui/profile/AiProfileWrap.kt b/app/src/main/java/com/aiosman/ravenow/ui/profile/AiProfileWrap.kt index 5f0c17c..5bb6044 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/profile/AiProfileWrap.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/profile/AiProfileWrap.kt @@ -1,22 +1,44 @@ package com.aiosman.ravenow.ui.profile import android.util.Log +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.aiosman.ravenow.GuestLoginCheckOut import com.aiosman.ravenow.GuestLoginCheckOutScene import com.aiosman.ravenow.LocalNavController +import com.aiosman.ravenow.R import com.aiosman.ravenow.data.UserServiceImpl import com.aiosman.ravenow.exp.viewModelFactory import com.aiosman.ravenow.ui.NavigationRoute +import com.aiosman.ravenow.ui.composables.PointsPaymentDialog import com.aiosman.ravenow.ui.index.tabs.ai.tabs.mine.MineAgentViewModel import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel import com.aiosman.ravenow.ui.navigateToChatAi import com.aiosman.ravenow.ui.navigateToPost import kotlinx.coroutines.launch +@OptIn(ExperimentalMaterial3Api::class) @Composable fun AiProfileWrap(id: String) { val model: AiProfileViewModel = viewModel(factory = viewModelFactory { @@ -26,6 +48,26 @@ fun AiProfileWrap(id: String) { val navController = LocalNavController.current val scope = rememberCoroutineScope() + // 记忆管理相关状态 + var showMemoryManageDialog by remember { mutableStateOf(false) } + var showAddMemoryDialog by remember { mutableStateOf(false) } + var showAddMemoryConfirmDialog by remember { mutableStateOf(false) } + var pendingMemoryText by remember { mutableStateOf("") } + val memoryManageSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val addMemorySheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + // 获取 chatAIId (openId) + val chatAIId = model.profile?.chatAIId ?: "" + + // 创建记忆管理 ViewModel + val memoryViewModel: AgentMemoryManageViewModel? = if (chatAIId.isNotEmpty()) { + viewModel(factory = viewModelFactory { + AgentMemoryManageViewModel(chatAIId) + }, key = "agentMemoryViewModel_${chatAIId}") + } else { + null + } + LaunchedEffect(Unit) { model.loadProfile(id) MyProfileViewModel.loadProfile() @@ -33,58 +75,174 @@ fun AiProfileWrap(id: String) { val isSelf = id == MyProfileViewModel.profile?.id.toString() - AiProfileV3( - profile = model.profile, - moments = model.moments, - postCount = model.momentLoader.total, - isSelf = isSelf, - onFollowClick = { - model.profile?.let { - if (it.isFollowing) { - model.unFollowUser(id) - } else { - model.followUser(id) + Box(modifier = Modifier.fillMaxSize()) { + AiProfileV3( + profile = model.profile, + moments = model.moments, + postCount = model.momentLoader.total, + isSelf = isSelf, + onFollowClick = { + model.profile?.let { + if (it.isFollowing) { + model.unFollowUser(id) + } else { + model.followUser(id) + } } - } - }, - onChatClick = { - // 检查游客模式 - if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.CHAT_WITH_AGENT)) { - navController.navigate(NavigationRoute.Login.route) - } else { - model.profile?.let { profile -> - scope.launch { - try { - // 参考主页逻辑:使用chatAIId作为openId创建单聊并导航 - // 创建单聊 - MineAgentViewModel.createSingleChat(profile.chatAIId) - - // 通过chatAIId获取完整的AI profile(类似主页的goToChatAi逻辑) - val userService = UserServiceImpl() - val aiProfile = userService.getUserProfileByOpenId(profile.chatAIId) - - // 导航到AI聊天页面 - navController.navigateToChatAi(aiProfile.id.toString()) - } catch (e: Exception) { - Log.e("AiProfileWrap", "Error navigating to AI chat", e) - // 如果获取失败,直接使用当前profile的id - navController.navigateToChatAi(profile.id.toString()) + }, + onChatClick = { + // 检查游客模式 + if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.CHAT_WITH_AGENT)) { + navController.navigate(NavigationRoute.Login.route) + } else { + model.profile?.let { profile -> + scope.launch { + try { + // 参考主页逻辑:使用chatAIId作为openId创建单聊并导航 + // 创建单聊 + MineAgentViewModel.createSingleChat(profile.chatAIId) + + // 通过chatAIId获取完整的AI profile(类似主页的goToChatAi逻辑) + val userService = UserServiceImpl() + val aiProfile = userService.getUserProfileByOpenId(profile.chatAIId) + + // 导航到AI聊天页面 + navController.navigateToChatAi(aiProfile.id.toString()) + } catch (e: Exception) { + Log.e("AiProfileWrap", "Error navigating to AI chat", e) + // 如果获取失败,直接使用当前profile的id + navController.navigateToChatAi(profile.id.toString()) + } } } } + }, + onShareClick = { + // TODO: 实现分享逻辑 + Log.d("AiProfileWrap", "分享功能待实现") + }, + onMemoryManageClick = { + if (chatAIId.isNotEmpty()) { + showMemoryManageDialog = true + } + }, + onLoadMore = { + Log.d("AiProfileWrap", "onLoadMore被调用") + model.loadMoreMoment() + }, + onComment = { moment -> + navController.navigateToPost(moment.id) + } + ) + + // 添加记忆对话框 + if (showAddMemoryDialog && memoryViewModel != null) { + ModalBottomSheet( + onDismissRequest = { showAddMemoryDialog = false }, + sheetState = addMemorySheetState, + containerColor = Color(0xFFFAF9FB), + dragHandle = { + Box( + modifier = Modifier + .width(36.dp) + .height(5.dp) + .padding(top = 5.dp) + .background( + Color(0xFFCCCCCC), + RoundedCornerShape(100.dp) + ) + ) + }, + shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp) + ) { + AddAgentMemoryDialog( + profile = model.profile, + viewModel = memoryViewModel, + onDismiss = { showAddMemoryDialog = false }, + onRequestAddMemory = { memoryText -> + // 关闭添加对话框,显示确认框 + showAddMemoryDialog = false + pendingMemoryText = memoryText + showAddMemoryConfirmDialog = true + } + ) } - }, - onShareClick = { - // TODO: 实现分享逻辑 - Log.d("AiProfileWrap", "分享功能待实现") - }, - onLoadMore = { - Log.d("AiProfileWrap", "onLoadMore被调用") - model.loadMoreMoment() - }, - onComment = { moment -> - navController.navigateToPost(moment.id) } - ) + + // 记忆管理弹窗 + if (showMemoryManageDialog && memoryViewModel != null && chatAIId.isNotEmpty()) { + ModalBottomSheet( + onDismissRequest = { showMemoryManageDialog = false }, + sheetState = memoryManageSheetState, + containerColor = Color(0xFFFAF9FB), + dragHandle = { + Box( + modifier = Modifier + .width(36.dp) + .height(5.dp) + .padding(top = 5.dp) + .background( + Color(0xFFCCCCCC), + RoundedCornerShape(100.dp) + ) + ) + }, + shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp) + ) { + // 立即展开到全屏,避免逐渐变高的动画 + LaunchedEffect(Unit) { + memoryManageSheetState.expand() + } + AgentMemoryManageContent( + openId = chatAIId, + profile = model.profile, + viewModel = memoryViewModel, + onAddMemoryClick = { + showMemoryManageDialog = false + showAddMemoryDialog = true + }, + onDismiss = { + showMemoryManageDialog = false + } + ) + } + } + + // 添加记忆确认对话框 + if (showAddMemoryConfirmDialog && memoryViewModel != null) { + val cost = memoryViewModel.addMemoryCost ?: 0 + val currentBalance = memoryViewModel.pointsBalance ?: 0 + val balanceAfterCost = (currentBalance - cost).coerceAtLeast(0) + val isBalanceSufficient = currentBalance >= cost + + // 监听添加成功,关闭确认对话框并刷新积分余额 + LaunchedEffect(memoryViewModel.addMemorySuccess) { + if (memoryViewModel.addMemorySuccess) { + showAddMemoryConfirmDialog = false + pendingMemoryText = "" + memoryViewModel.addMemorySuccess = false + memoryViewModel.loadPointsBalance() // 刷新积分余额 + } + } + + PointsPaymentDialog( + cost = cost, + currentBalance = currentBalance, + balanceAfterCost = balanceAfterCost, + isBalanceSufficient = isBalanceSufficient, + onConfirm = { + // 确认支付,添加记忆 + memoryViewModel.addAgentMemory(pendingMemoryText) + }, + onCancel = { + showAddMemoryConfirmDialog = false + pendingMemoryText = "" + }, + title = stringResource(R.string.group_chat_info_add_memory), + description = stringResource(R.string.group_chat_info_memory_description), + isProcessing = memoryViewModel.isAddingMemory + ) + } + } }