Merge pull request #76 from Kevinlinpr/atm2

Refactor: 群记忆重构为房间规则并新增私密群组付费功能
This commit is contained in:
2025-11-13 10:12:20 +08:00
committed by GitHub
11 changed files with 1537 additions and 249 deletions

View File

@@ -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
)
}

View File

@@ -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
)
/**

View File

@@ -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,
)

View File

@@ -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
)
/**

View File

@@ -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
)
}
}
}

View File

@@ -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<AgentRuleQuota?>(null)
var memoryList by mutableStateOf<List<AgentRule>>(emptyList())
var memoryQuota by mutableStateOf<RoomRuleQuotaEntity?>(null)
var memoryList by mutableStateOf<List<RoomRuleEntity>>(emptyList())
var isLoadingMemory by mutableStateOf(false)
var memoryError by mutableStateOf<String?>(null)
var promptOpenId by mutableStateOf<String?>(null)
// 房间规则服务
private val roomService: RoomService = RoomServiceImpl()
// 群可见性相关状态
var privateGroupCost by mutableStateOf<Int?>(null)
var pointsBalance by mutableStateOf<Int?>(null)
var isLoadingVisibility by mutableStateOf(false)
var isUpdatingVisibility by mutableStateOf(false)
// 群记忆相关状态
var addMemoryCost by mutableStateOf<Int?>(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)
}
}
}
}

View File

@@ -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(

View File

@@ -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<Int?>(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)
)
)
}
}
}
}
}

View File

@@ -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<AgentRuleQuotaEntity?>(null)
var memoryList by mutableStateOf<List<AgentRuleEntity>>(emptyList())
var isLoadingMemory by mutableStateOf(false)
var memoryError by mutableStateOf<String?>(null)
// Agent 规则服务
private val agentRuleService: AgentRuleService = AgentRuleServiceImpl()
// 添加记忆相关状态
var isAddingMemory by mutableStateOf(false)
var addMemoryError by mutableStateOf<String?>(null)
var addMemorySuccess by mutableStateOf(false)
var addMemoryCost by mutableStateOf<Int?>(null)
var pointsBalance by mutableStateOf<Int?>(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)
}
}
}
}

View File

@@ -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(

View File

@@ -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
)
}
}
}