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