Enhance AI Agent Profile Interaction

This commit introduces several enhancements to how AI agent profiles are displayed and interacted with:

**Profile Display:**
- **AI Account Distinction:** Profile pages now differentiate between regular user accounts and AI agent accounts.
    - AI agent profiles will not display the "Agents" tab in their profile.
    - The profile header height is adjusted for AI accounts.
- **Navigation Parameter:** An `isAiAccount` boolean parameter is added to the `AccountProfile` navigation route to indicate if the profile being viewed belongs to an AI.

**Interaction & Navigation:**
- **Avatar Click Navigation:**
    - Clicking an AI agent's avatar in various lists (Hot Agents, My Agents, User Agents Row, User Agents List) now navigates to the agent's dedicated profile page.
    - When navigating to an agent's profile from an agent list, `isAiAccount` is set to `true`.
- **Chat Initiation:** Clicking the chat button on AI agent cards in the "Agent" tab (both Hot and My Agents) now correctly initiates a chat with the respective AI.
- **ViewModel Updates:**
    - `AgentViewModel`, `MineAgentViewModel`, and `HotAgentViewModel` now include a `goToProfile` function to handle navigation to agent profiles, correctly passing the `isAiAccount` flag.

**Code Refinements:**
- Click handlers for agent avatars and chat buttons are now wrapped with `DebounceUtils.simpleDebounceClick` to prevent multiple rapid clicks.
- The `UserContentPageIndicator` now conditionally hides the "Agent" tab based on the `isAiAccount` status.
- `UserAgentsRow` and `UserAgentsList` now accept an `onAvatarClick` callback for navigating to agent profiles.
- `AgentItem` (used in `UserAgentsRow`) and `UserAgentCard` (used in `UserAgentsList`) now handle avatar clicks.
- The general `Agent` composable (used in `AiPostComposable`) now also supports an `onAvatarClick` callback.
This commit is contained in:
2025-09-01 01:10:35 +08:00
parent 12b3c15083
commit 484d641554
14 changed files with 200 additions and 56 deletions

View File

@@ -85,7 +85,7 @@ sealed class NavigationRoute(
data object NewPost : NavigationRoute("NewPost") data object NewPost : NavigationRoute("NewPost")
data object EditModification : NavigationRoute("EditModification") data object EditModification : NavigationRoute("EditModification")
data object Login : NavigationRoute("Login") data object Login : NavigationRoute("Login")
data object AccountProfile : NavigationRoute("AccountProfile/{id}") data object AccountProfile : NavigationRoute("AccountProfile/{id}?isAiAccount={isAiAccount}")
data object SignUp : NavigationRoute("SignUp") data object SignUp : NavigationRoute("SignUp")
data object UserAuth : NavigationRoute("UserAuth") data object UserAuth : NavigationRoute("UserAuth")
data object EmailSignUp : NavigationRoute("EmailSignUp") data object EmailSignUp : NavigationRoute("EmailSignUp")
@@ -284,7 +284,13 @@ fun NavigationController(
} }
composable( composable(
route = NavigationRoute.AccountProfile.route, route = NavigationRoute.AccountProfile.route,
arguments = listOf(navArgument("id") { type = NavType.StringType }), arguments = listOf(
navArgument("id") { type = NavType.StringType },
navArgument("isAiAccount") {
type = NavType.BoolType
defaultValue = false
}
),
enterTransition = { enterTransition = {
// iOS push: new screen slides in from the right // iOS push: new screen slides in from the right
slideInHorizontally( slideInHorizontally(
@@ -317,7 +323,9 @@ fun NavigationController(
CompositionLocalProvider( CompositionLocalProvider(
LocalAnimatedContentScope provides this, LocalAnimatedContentScope provides this,
) { ) {
AccountProfileV2(it.arguments?.getString("id")!!) val id = it.arguments?.getString("id")!!
val isAiAccount = it.arguments?.getBoolean("isAiAccount") ?: false
AccountProfileV2(id, isAiAccount)
} }
} }
composable( composable(

View File

@@ -42,6 +42,7 @@ fun AgentCard(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
agentEntity: AgentEntity, agentEntity: AgentEntity,
onClick: () -> Unit = {}, onClick: () -> Unit = {},
onAvatarClick: () -> Unit = {},
) { ) {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val context = LocalContext.current val context = LocalContext.current
@@ -63,6 +64,9 @@ fun AgentCard(
modifier = Modifier modifier = Modifier
.size(40.dp) .size(40.dp)
.clip(RoundedCornerShape(40.dp)) .clip(RoundedCornerShape(40.dp))
.noRippleClickable {
onAvatarClick()
}
) { ) {
CustomAsyncImage( CustomAsyncImage(
context, context,

View File

@@ -389,7 +389,14 @@ fun AgentCard2(viewModel: AgentViewModel,agentItem: AgentItem,navController: Nav
Box( Box(
modifier = Modifier modifier = Modifier
.size(48.dp) .size(48.dp)
.background(Color(0xFFF5F5F5), RoundedCornerShape(24.dp)), .background(Color(0xFFF5F5F5), RoundedCornerShape(24.dp))
.clickable {
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
viewModel.goToProfile(agentItem.openId, navController)
}) {
lastClickTime = System.currentTimeMillis()
}
},
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
if (agentItem.avatar.isNotEmpty()) { if (agentItem.avatar.isNotEmpty()) {
@@ -451,8 +458,8 @@ fun AgentCard2(viewModel: AgentViewModel,agentItem: AgentItem,navController: Nav
) )
.clickable { .clickable {
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) { if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
/* viewModel.createSingleChat(agentItem.trtcId) viewModel.createSingleChat(agentItem.openId)
viewModel.goToChatAi(agentItem.trtcId, navController = navController)*/ viewModel.goToChatAi(agentItem.openId, navController = navController)
}) { }) {
lastClickTime = System.currentTimeMillis() lastClickTime = System.currentTimeMillis()
} }

View File

@@ -10,6 +10,7 @@ import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.data.api.RaveNowAPI import com.aiosman.ravenow.data.api.RaveNowAPI
import com.aiosman.ravenow.data.api.SingleChatRequestBody import com.aiosman.ravenow.data.api.SingleChatRequestBody
import com.aiosman.ravenow.ui.index.tabs.ai.tabs.mine.MineAgentViewModel.createGroup2ChatAi import com.aiosman.ravenow.ui.index.tabs.ai.tabs.mine.MineAgentViewModel.createGroup2ChatAi
import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.index.tabs.message.MessageListViewModel.userService import com.aiosman.ravenow.ui.index.tabs.message.MessageListViewModel.userService
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre.AgentItem import com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre.AgentItem
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -75,4 +76,23 @@ object AgentViewModel: ViewModel() {
} }
} }
fun goToProfile(
openId: String,
navController: NavHostController
) {
viewModelScope.launch {
try {
val profile = userService.getUserProfileByOpenId(openId)
// 从Agent列表点击进去的一定是智能体直接传递isAiAccount = true
navController.navigate(
NavigationRoute.AccountProfile.route
.replace("{id}", profile.id.toString())
.replace("{isAiAccount}", "true")
)
} catch (e: Exception) {
// swallow error to avoid crash on navigation attempt failures
}
}
}
} }

View File

@@ -120,6 +120,9 @@ fun HotAgent() {
model.createSingleChat(agentItem.openId) model.createSingleChat(agentItem.openId)
model.goToChatAi(agentItem.openId,navController) model.goToChatAi(agentItem.openId,navController)
}, },
onAvatarClick = {
model.goToProfile(agentItem.openId, navController)
}
) )
} }

View File

@@ -12,6 +12,7 @@ import com.aiosman.ravenow.data.api.SingleChatRequestBody
import com.aiosman.ravenow.entity.AgentEntity import com.aiosman.ravenow.entity.AgentEntity
import com.aiosman.ravenow.ui.index.tabs.ai.tabs.mine.MineAgentViewModel.createGroup2ChatAi import com.aiosman.ravenow.ui.index.tabs.ai.tabs.mine.MineAgentViewModel.createGroup2ChatAi
import com.aiosman.ravenow.ui.index.tabs.message.MessageListViewModel.userService import com.aiosman.ravenow.ui.index.tabs.message.MessageListViewModel.userService
import com.aiosman.ravenow.ui.NavigationRoute
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
object HotAgentViewModel : ViewModel() { object HotAgentViewModel : ViewModel() {
@@ -128,6 +129,25 @@ object HotAgentViewModel : ViewModel() {
} }
} }
fun goToProfile(
openId: String,
navController: NavHostController
) {
viewModelScope.launch {
try {
val profile = userService.getUserProfileByOpenId(openId)
// 从Agent列表点击进去的一定是智能体直接传递isAiAccount = true
navController.navigate(
NavigationRoute.AccountProfile.route
.replace("{id}", profile.id.toString())
.replace("{isAiAccount}", "true")
)
} catch (e: Exception) {
// swallow error to avoid crash on navigation attempt failures
}
}
}
// 预加载图片,避免滑动时重复加载 // 预加载图片,避免滑动时重复加载
fun preloadImages(context: android.content.Context) { fun preloadImages(context: android.content.Context) {
viewModelScope.launch { viewModelScope.launch {

View File

@@ -111,6 +111,9 @@ fun MineAgent() {
model.createSingleChat(agent.openId) model.createSingleChat(agent.openId)
model.goToChatAi(agent.openId,navController) model.goToChatAi(agent.openId,navController)
}, },
onAvatarClick = {
model.goToProfile(agent.openId, navController)
}
) )
} }
} }

View File

@@ -26,6 +26,7 @@ import com.aiosman.ravenow.entity.MomentRemoteDataSource
import com.aiosman.ravenow.entity.MomentServiceImpl import com.aiosman.ravenow.entity.MomentServiceImpl
import com.aiosman.ravenow.ui.index.tabs.message.MessageListViewModel.userService import com.aiosman.ravenow.ui.index.tabs.message.MessageListViewModel.userService
import com.aiosman.ravenow.ui.navigateToChatAi import com.aiosman.ravenow.ui.navigateToChatAi
import com.aiosman.ravenow.ui.NavigationRoute
import com.tencent.imsdk.v2.V2TIMConversationOperationResult import com.tencent.imsdk.v2.V2TIMConversationOperationResult
import com.tencent.imsdk.v2.V2TIMManager import com.tencent.imsdk.v2.V2TIMManager
import com.tencent.imsdk.v2.V2TIMValueCallback import com.tencent.imsdk.v2.V2TIMValueCallback
@@ -119,5 +120,24 @@ object MineAgentViewModel : ViewModel() {
} }
} }
fun goToProfile(
openId: String,
navController: NavHostController
) {
viewModelScope.launch {
try {
val profile = userService.getUserProfileByOpenId(openId)
// 从Agent列表点击进去的一定是智能体直接传递isAiAccount = true
navController.navigate(
NavigationRoute.AccountProfile.route
.replace("{id}", profile.id.toString())
.replace("{isAiAccount}", "true")
)
} catch (e: Exception) {
// swallow error to avoid crash on navigation attempt failures
}
}
}
} }

View File

@@ -101,6 +101,7 @@ fun ProfileV3(
agents: List<AgentEntity> = emptyList(), agents: List<AgentEntity> = emptyList(),
isSelf: Boolean = true, isSelf: Boolean = true,
isMain:Boolean = false, isMain:Boolean = false,
isAiAccount: Boolean = false, // 新增参数判断是否为AI账户
onLoadMore: () -> Unit = {}, onLoadMore: () -> Unit = {},
onLike: (MomentEntity) -> Unit = {}, onLike: (MomentEntity) -> Unit = {},
onComment: (MomentEntity) -> Unit = {}, onComment: (MomentEntity) -> Unit = {},
@@ -109,7 +110,7 @@ fun ProfileV3(
) { ) {
val model = MyProfileViewModel val model = MyProfileViewModel
val state = rememberCollapsingToolbarScaffoldState() val state = rememberCollapsingToolbarScaffoldState()
val pagerState = rememberPagerState(pageCount = { 2 }) val pagerState = rememberPagerState(pageCount = { if (isAiAccount) 1 else 2 })
val enabled by remember { mutableStateOf(true) } val enabled by remember { mutableStateOf(true) }
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues() val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
@@ -227,7 +228,7 @@ fun ProfileV3(
modifier = Modifier modifier = Modifier
.parallax(0.5f) .parallax(0.5f)
.fillMaxWidth() .fillMaxWidth()
.height(700.dp) .height(if (isAiAccount) 600.dp else 700.dp)
.background(AppColors.profileBackground) .background(AppColors.profileBackground)
.verticalScroll(toolbarScrollState) .verticalScroll(toolbarScrollState)
) { ) {
@@ -387,7 +388,8 @@ fun ProfileV3(
} }
} }
// 添加用户智能体行 // 添加用户智能体行(智能体用户不显示)
if (!isAiAccount) {
UserAgentsRow( UserAgentsRow(
userId = if (isSelf) null else profile?.id, userId = if (isSelf) null else profile?.id,
modifier = Modifier.padding(top = 16.dp), modifier = Modifier.padding(top = 16.dp),
@@ -398,9 +400,26 @@ fun ProfileV3(
onAgentClick = { agent -> onAgentClick = { agent ->
// 导航到智能体详情页面 // 导航到智能体详情页面
// TODO: 实现导航逻辑 // TODO: 实现导航逻辑
},
onAvatarClick = { agent ->
// 导航到智能体个人主页
scope.launch {
try {
val userService = com.aiosman.ravenow.data.UserServiceImpl()
val profile = userService.getUserProfileByOpenId(agent.openId)
navController.navigate(
NavigationRoute.AccountProfile.route
.replace("{id}", profile.id.toString())
.replace("{isAiAccount}", "true")
)
} catch (e: Exception) {
// 处理错误
}
}
} }
) )
} }
}
} }
@@ -497,6 +516,7 @@ fun ProfileV3(
) { ) {
UserContentPageIndicator( UserContentPageIndicator(
pagerState = pagerState, pagerState = pagerState,
showAgentTab = !isAiAccount
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
HorizontalPager( HorizontalPager(
@@ -555,6 +575,22 @@ fun ProfileV3(
UserAgentsList( UserAgentsList(
agents = agents, agents = agents,
onAgentClick = onAgentClick, onAgentClick = onAgentClick,
onAvatarClick = { agent ->
// 导航到智能体个人主页需要通过openId获取用户ID
scope.launch {
try {
val userService = com.aiosman.ravenow.data.UserServiceImpl()
val profile = userService.getUserProfileByOpenId(agent.openId)
navController.navigate(
NavigationRoute.AccountProfile.route
.replace("{id}", profile.id.toString())
.replace("{isAiAccount}", "true")
)
} catch (e: Exception) {
// 处理错误
}
}
},
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) )
} }

View File

@@ -18,6 +18,7 @@ fun ProfileWrap(
} }
ProfileV3( ProfileV3(
isMain = true, isMain = true,
isAiAccount = MyProfileViewModel.profile?.aiAccount == true, // 传入AI账户判断
postCount = MyProfileViewModel.momentLoader.total, postCount = MyProfileViewModel.momentLoader.total,
onUpdateBanner = { uri, file, context -> onUpdateBanner = { uri, file, context ->
MyProfileViewModel.updateUserProfileBanner(uri, file, context) MyProfileViewModel.updateUserProfileBanner(uri, file, context)

View File

@@ -45,6 +45,7 @@ import com.aiosman.ravenow.utils.DebounceUtils
fun UserAgentsList( fun UserAgentsList(
agents: List<AgentEntity>, agents: List<AgentEntity>,
onAgentClick: (AgentEntity) -> Unit = {}, onAgentClick: (AgentEntity) -> Unit = {},
onAvatarClick: (AgentEntity) -> Unit = {},
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
@@ -61,7 +62,8 @@ fun UserAgentsList(
items(agents) { agent -> items(agents) { agent ->
UserAgentCard( UserAgentCard(
agent = agent, agent = agent,
onAgentClick = onAgentClick onAgentClick = onAgentClick,
onAvatarClick = onAvatarClick
) )
} }
} }
@@ -76,7 +78,8 @@ fun UserAgentsList(
@Composable @Composable
fun UserAgentCard( fun UserAgentCard(
agent: AgentEntity, agent: AgentEntity,
onAgentClick: (AgentEntity) -> Unit onAgentClick: (AgentEntity) -> Unit,
onAvatarClick: (AgentEntity) -> Unit = {}
) { ) {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
@@ -93,7 +96,14 @@ fun UserAgentCard(
Box( Box(
modifier = Modifier modifier = Modifier
.size(48.dp) .size(48.dp)
.background(AppColors.nonActive, RoundedCornerShape(24.dp)), .background(AppColors.nonActive, RoundedCornerShape(24.dp))
.clickable {
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
onAvatarClick(agent)
}) {
lastClickTime = System.currentTimeMillis()
}
},
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
if (agent.avatar.isNotEmpty()) { if (agent.avatar.isNotEmpty()) {

View File

@@ -45,7 +45,8 @@ fun UserAgentsRow(
userId: Int?, userId: Int?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onMoreClick: () -> Unit = {}, onMoreClick: () -> Unit = {},
onAgentClick: (AgentEntity) -> Unit = {} onAgentClick: (AgentEntity) -> Unit = {},
onAvatarClick: (AgentEntity) -> Unit = {}
) { ) {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val viewModel: UserAgentsViewModel = viewModel(key = "UserAgentsViewModel_${userId ?: "self"}") val viewModel: UserAgentsViewModel = viewModel(key = "UserAgentsViewModel_${userId ?: "self"}")
@@ -128,7 +129,8 @@ fun UserAgentsRow(
items(viewModel.agents) { agent -> items(viewModel.agents) { agent ->
AgentItem( AgentItem(
agent = agent, agent = agent,
onClick = { onAgentClick(agent) } onClick = { onAgentClick(agent) },
onAvatarClick = { onAvatarClick(agent) }
) )
} }
@@ -150,19 +152,21 @@ fun UserAgentsRow(
private fun AgentItem( private fun AgentItem(
agent: AgentEntity, agent: AgentEntity,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onClick: () -> Unit = {} onClick: () -> Unit = {},
onAvatarClick: () -> Unit = {}
) { ) {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier.noRippleClickable { onClick() } modifier = modifier
) { ) {
// 头像 // 头像
Box( Box(
modifier = Modifier modifier = Modifier
.size(48.dp) .size(48.dp)
.clip(CircleShape) .clip(CircleShape)
.noRippleClickable { onAvatarClick() }
) { ) {
CustomAsyncImage( CustomAsyncImage(
context = LocalContext.current, context = LocalContext.current,
@@ -185,7 +189,9 @@ private fun AgentItem(
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
modifier = Modifier.width(48.dp) modifier = Modifier
.width(48.dp)
.noRippleClickable { onClick() }
) )
} }
} }

View File

@@ -37,7 +37,8 @@ import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun UserContentPageIndicator( fun UserContentPageIndicator(
pagerState: PagerState pagerState: PagerState,
showAgentTab: Boolean = true
){ ){
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
@@ -71,7 +72,8 @@ fun UserContentPageIndicator(
) )
} }
// Agent Tab // Agent Tab (只在非智能体用户时显示)
if (showAgentTab) {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier modifier = Modifier
@@ -91,6 +93,7 @@ fun UserContentPageIndicator(
) )
} }
} }
}
// 下划线指示器 // 下划线指示器
Row( Row(
@@ -106,6 +109,7 @@ fun UserContentPageIndicator(
if (pagerState.currentPage == 0) AppColors.text else Color.Transparent if (pagerState.currentPage == 0) AppColors.text else Color.Transparent
) )
) )
if (showAgentTab) {
Box( Box(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
@@ -117,3 +121,4 @@ fun UserContentPageIndicator(
} }
} }
} }
}

View File

@@ -12,7 +12,7 @@ import com.aiosman.ravenow.ui.navigateToChat
import com.aiosman.ravenow.ui.navigateToPost import com.aiosman.ravenow.ui.navigateToPost
@Composable @Composable
fun AccountProfileV2(id: String){ fun AccountProfileV2(id: String, isAiAccount: Boolean = false){
val model: AccountProfileViewModel = viewModel(factory = viewModelFactory { val model: AccountProfileViewModel = viewModel(factory = viewModelFactory {
AccountProfileViewModel() AccountProfileViewModel()
}, key = "viewModel_${id}") }, key = "viewModel_${id}")
@@ -30,6 +30,7 @@ fun AccountProfileV2(id: String){
agents = model.agents, agents = model.agents,
profile = model.profile, profile = model.profile,
isSelf = isSelf, isSelf = isSelf,
isAiAccount = isAiAccount, // 从参数传入AI账户判断
postCount = model.momentLoader.total, postCount = model.momentLoader.total,
onLoadMore = { onLoadMore = {
Log.d("AccountProfileV2", "onLoadMore被调用") Log.d("AccountProfileV2", "onLoadMore被调用")