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 EditModification : NavigationRoute("EditModification")
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 UserAuth : NavigationRoute("UserAuth")
data object EmailSignUp : NavigationRoute("EmailSignUp")
@@ -284,7 +284,13 @@ fun NavigationController(
}
composable(
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 = {
// iOS push: new screen slides in from the right
slideInHorizontally(
@@ -317,7 +323,9 @@ fun NavigationController(
CompositionLocalProvider(
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(

View File

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

View File

@@ -389,7 +389,14 @@ fun AgentCard2(viewModel: AgentViewModel,agentItem: AgentItem,navController: Nav
Box(
modifier = Modifier
.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
) {
if (agentItem.avatar.isNotEmpty()) {
@@ -451,8 +458,8 @@ fun AgentCard2(viewModel: AgentViewModel,agentItem: AgentItem,navController: Nav
)
.clickable {
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
/* viewModel.createSingleChat(agentItem.trtcId)
viewModel.goToChatAi(agentItem.trtcId, navController = navController)*/
viewModel.createSingleChat(agentItem.openId)
viewModel.goToChatAi(agentItem.openId, navController = navController)
}) {
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.SingleChatRequestBody
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.moment.tabs.expolre.AgentItem
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.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.ui.index.tabs.ai.tabs.mine.MineAgentViewModel.createGroup2ChatAi
import com.aiosman.ravenow.ui.index.tabs.message.MessageListViewModel.userService
import com.aiosman.ravenow.ui.NavigationRoute
import kotlinx.coroutines.launch
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) {
viewModelScope.launch {

View File

@@ -111,6 +111,9 @@ fun MineAgent() {
model.createSingleChat(agent.openId)
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.ui.index.tabs.message.MessageListViewModel.userService
import com.aiosman.ravenow.ui.navigateToChatAi
import com.aiosman.ravenow.ui.NavigationRoute
import com.tencent.imsdk.v2.V2TIMConversationOperationResult
import com.tencent.imsdk.v2.V2TIMManager
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(),
isSelf: Boolean = true,
isMain:Boolean = false,
isAiAccount: Boolean = false, // 新增参数判断是否为AI账户
onLoadMore: () -> Unit = {},
onLike: (MomentEntity) -> Unit = {},
onComment: (MomentEntity) -> Unit = {},
@@ -109,7 +110,7 @@ fun ProfileV3(
) {
val model = MyProfileViewModel
val state = rememberCollapsingToolbarScaffoldState()
val pagerState = rememberPagerState(pageCount = { 2 })
val pagerState = rememberPagerState(pageCount = { if (isAiAccount) 1 else 2 })
val enabled by remember { mutableStateOf(true) }
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
var expanded by remember { mutableStateOf(false) }
@@ -227,7 +228,7 @@ fun ProfileV3(
modifier = Modifier
.parallax(0.5f)
.fillMaxWidth()
.height(700.dp)
.height(if (isAiAccount) 600.dp else 700.dp)
.background(AppColors.profileBackground)
.verticalScroll(toolbarScrollState)
) {
@@ -387,19 +388,37 @@ fun ProfileV3(
}
}
// 添加用户智能体行
UserAgentsRow(
userId = if (isSelf) null else profile?.id,
modifier = Modifier.padding(top = 16.dp),
onMoreClick = {
// 导航到智能体列表页面
// TODO: 实现导航逻辑
},
onAgentClick = { agent ->
// 导航到智能体详情页面
// TODO: 实现导航逻辑
}
)
// 添加用户智能体行(智能体用户不显示)
if (!isAiAccount) {
UserAgentsRow(
userId = if (isSelf) null else profile?.id,
modifier = Modifier.padding(top = 16.dp),
onMoreClick = {
// 导航到智能体列表页面
// TODO: 实现导航逻辑
},
onAgentClick = { agent ->
// 导航到智能体详情页面
// 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(
pagerState = pagerState,
showAgentTab = !isAiAccount
)
Spacer(modifier = Modifier.height(8.dp))
HorizontalPager(
@@ -555,6 +575,22 @@ fun ProfileV3(
UserAgentsList(
agents = agents,
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()
)
}

View File

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

View File

@@ -45,6 +45,7 @@ import com.aiosman.ravenow.utils.DebounceUtils
fun UserAgentsList(
agents: List<AgentEntity>,
onAgentClick: (AgentEntity) -> Unit = {},
onAvatarClick: (AgentEntity) -> Unit = {},
modifier: Modifier = Modifier
) {
val AppColors = LocalAppTheme.current
@@ -61,7 +62,8 @@ fun UserAgentsList(
items(agents) { agent ->
UserAgentCard(
agent = agent,
onAgentClick = onAgentClick
onAgentClick = onAgentClick,
onAvatarClick = onAvatarClick
)
}
}
@@ -76,7 +78,8 @@ fun UserAgentsList(
@Composable
fun UserAgentCard(
agent: AgentEntity,
onAgentClick: (AgentEntity) -> Unit
onAgentClick: (AgentEntity) -> Unit,
onAvatarClick: (AgentEntity) -> Unit = {}
) {
val AppColors = LocalAppTheme.current
@@ -93,7 +96,14 @@ fun UserAgentCard(
Box(
modifier = Modifier
.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
) {
if (agent.avatar.isNotEmpty()) {

View File

@@ -45,7 +45,8 @@ fun UserAgentsRow(
userId: Int?,
modifier: Modifier = Modifier,
onMoreClick: () -> Unit = {},
onAgentClick: (AgentEntity) -> Unit = {}
onAgentClick: (AgentEntity) -> Unit = {},
onAvatarClick: (AgentEntity) -> Unit = {}
) {
val AppColors = LocalAppTheme.current
val viewModel: UserAgentsViewModel = viewModel(key = "UserAgentsViewModel_${userId ?: "self"}")
@@ -128,7 +129,8 @@ fun UserAgentsRow(
items(viewModel.agents) { agent ->
AgentItem(
agent = agent,
onClick = { onAgentClick(agent) }
onClick = { onAgentClick(agent) },
onAvatarClick = { onAvatarClick(agent) }
)
}
@@ -150,19 +152,21 @@ fun UserAgentsRow(
private fun AgentItem(
agent: AgentEntity,
modifier: Modifier = Modifier,
onClick: () -> Unit = {}
onClick: () -> Unit = {},
onAvatarClick: () -> Unit = {}
) {
val AppColors = LocalAppTheme.current
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier.noRippleClickable { onClick() }
modifier = modifier
) {
// 头像
Box(
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.noRippleClickable { onAvatarClick() }
) {
CustomAsyncImage(
context = LocalContext.current,
@@ -185,7 +189,9 @@ private fun AgentItem(
textAlign = TextAlign.Center,
maxLines = 1,
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)
@Composable
fun UserContentPageIndicator(
pagerState: PagerState
pagerState: PagerState,
showAgentTab: Boolean = true
){
val scope = rememberCoroutineScope()
val AppColors = LocalAppTheme.current
@@ -71,24 +72,26 @@ fun UserContentPageIndicator(
)
}
// Agent Tab
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.weight(1f)
.noRippleClickable {
scope.launch {
pagerState.scrollToPage(1)
// Agent Tab (只在非智能体用户时显示)
if (showAgentTab) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.weight(1f)
.noRippleClickable {
scope.launch {
pagerState.scrollToPage(1)
}
}
}
.padding(vertical = 12.dp)
) {
Icon(
painter = painterResource(id = R.drawable.rider_pro_nav_ai),
contentDescription = "Agents",
tint = if (pagerState.currentPage == 1) AppColors.text else AppColors.text.copy(alpha = 0.6f),
modifier = Modifier.size(24.dp)
)
.padding(vertical = 12.dp)
) {
Icon(
painter = painterResource(id = R.drawable.rider_pro_nav_ai),
contentDescription = "Agents",
tint = if (pagerState.currentPage == 1) AppColors.text else AppColors.text.copy(alpha = 0.6f),
modifier = Modifier.size(24.dp)
)
}
}
}
@@ -106,14 +109,16 @@ fun UserContentPageIndicator(
if (pagerState.currentPage == 0) AppColors.text else Color.Transparent
)
)
Box(
modifier = Modifier
.weight(1f)
.height(2.dp)
.background(
if (pagerState.currentPage == 1) AppColors.text else Color.Transparent
)
)
if (showAgentTab) {
Box(
modifier = Modifier
.weight(1f)
.height(2.dp)
.background(
if (pagerState.currentPage == 1) AppColors.text else Color.Transparent
)
)
}
}
}
}

View File

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