@@ -5,7 +5,8 @@ object ConstVars {
|
|||||||
// Debug: http://192.168.0.201:8088
|
// Debug: http://192.168.0.201:8088
|
||||||
// Release: https://rider-pro.aiosman.com/beta_api
|
// Release: https://rider-pro.aiosman.com/beta_api
|
||||||
val BASE_SERVER = if (BuildConfig.DEBUG) {
|
val BASE_SERVER = if (BuildConfig.DEBUG) {
|
||||||
"http://47.109.137.67:6363" // Debug环境
|
// "http://47.109.137.67:6363" // Debug环境
|
||||||
|
"https://rider-pro.aiosman.com/beta_api" // Release环境
|
||||||
} else {
|
} else {
|
||||||
"https://rider-pro.aiosman.com/beta_api" // Release环境
|
"https://rider-pro.aiosman.com/beta_api" // Release环境
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -271,6 +271,13 @@ data class RemoveAccountRequestBody(
|
|||||||
val password: String,
|
val password: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class CategoryTranslation(
|
||||||
|
@SerializedName("name")
|
||||||
|
val name: String?,
|
||||||
|
@SerializedName("description")
|
||||||
|
val description: String?
|
||||||
|
)
|
||||||
|
|
||||||
data class CategoryTemplate(
|
data class CategoryTemplate(
|
||||||
@SerializedName("id")
|
@SerializedName("id")
|
||||||
val id: Int,
|
val id: Int,
|
||||||
@@ -295,8 +302,58 @@ data class CategoryTemplate(
|
|||||||
@SerializedName("createdAt")
|
@SerializedName("createdAt")
|
||||||
val createdAt: String,
|
val createdAt: String,
|
||||||
@SerializedName("updatedAt")
|
@SerializedName("updatedAt")
|
||||||
val updatedAt: String
|
val updatedAt: String,
|
||||||
)
|
@SerializedName("translations")
|
||||||
|
val translations: Map<String, CategoryTranslation>?
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* 根据语言代码获取翻译后的名称,如果没有翻译则返回默认名称
|
||||||
|
*/
|
||||||
|
fun getLocalizedName(lang: String): String {
|
||||||
|
// 尝试获取完整的语言标记(如 "zh-CN")
|
||||||
|
val translation = translations?.get(lang)
|
||||||
|
if (translation?.name != null && translation.name.isNotEmpty()) {
|
||||||
|
return translation.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有找到,尝试语言代码的前缀(如 "zh")
|
||||||
|
val langPrefix = lang.split("-", "_").firstOrNull()
|
||||||
|
if (langPrefix != null) {
|
||||||
|
translations?.entries?.forEach { (key, value) ->
|
||||||
|
if (key.startsWith(langPrefix) && value.name != null && value.name.isNotEmpty()) {
|
||||||
|
return value.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有翻译,返回默认名称
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据语言代码获取翻译后的描述,如果没有翻译则返回默认描述
|
||||||
|
*/
|
||||||
|
fun getLocalizedDescription(lang: String): String {
|
||||||
|
// 尝试获取完整的语言标记(如 "zh-CN")
|
||||||
|
val translation = translations?.get(lang)
|
||||||
|
if (translation?.description != null && translation.description.isNotEmpty()) {
|
||||||
|
return translation.description
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有找到,尝试语言代码的前缀(如 "zh")
|
||||||
|
val langPrefix = lang.split("-", "_").firstOrNull()
|
||||||
|
if (langPrefix != null) {
|
||||||
|
translations?.entries?.forEach { (key, value) ->
|
||||||
|
if (key.startsWith(langPrefix) && value.description != null && value.description.isNotEmpty()) {
|
||||||
|
return value.description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有翻译,返回默认描述
|
||||||
|
return description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class CategoryListResponse(
|
data class CategoryListResponse(
|
||||||
@SerializedName("page")
|
@SerializedName("page")
|
||||||
@@ -591,6 +648,7 @@ interface RaveNowAPI {
|
|||||||
@Query("withWorkflow") withWorkflow: Int = 1,
|
@Query("withWorkflow") withWorkflow: Int = 1,
|
||||||
@Query("authorId") authorId: Int? = null,
|
@Query("authorId") authorId: Int? = null,
|
||||||
@Query("categoryIds") categoryIds: List<Int>? = null,
|
@Query("categoryIds") categoryIds: List<Int>? = null,
|
||||||
|
@Query("random") random: Int? = null,
|
||||||
): Response<DataContainer<ListContainer<Agent>>>
|
): Response<DataContainer<ListContainer<Agent>>>
|
||||||
|
|
||||||
@GET("outside/my/prompts")
|
@GET("outside/my/prompts")
|
||||||
@@ -654,8 +712,9 @@ interface RaveNowAPI {
|
|||||||
@Query("withChildren") withChildren: Boolean? = null,
|
@Query("withChildren") withChildren: Boolean? = null,
|
||||||
@Query("withParent") withParent: Boolean? = null,
|
@Query("withParent") withParent: Boolean? = null,
|
||||||
@Query("withCount") withCount: Boolean? = null,
|
@Query("withCount") withCount: Boolean? = null,
|
||||||
@Query("hideEmpty") hideEmpty: Boolean? = null
|
@Query("hideEmpty") hideEmpty: Boolean? = null,
|
||||||
): Response<DataContainer<CategoryListResponse>>
|
@Query("lang") lang: String? = null
|
||||||
|
): Response<CategoryListResponse>
|
||||||
|
|
||||||
@GET("outside/categories/tree")
|
@GET("outside/categories/tree")
|
||||||
suspend fun getCategoryTree(
|
suspend fun getCategoryTree(
|
||||||
|
|||||||
@@ -146,11 +146,14 @@ fun Agent() {
|
|||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = {
|
title = {
|
||||||
androidx.compose.material3.Text(
|
Image(
|
||||||
text = "Rave AI",
|
painter = painterResource(id = R.drawable.home_logo),
|
||||||
fontSize = 20.sp,
|
contentDescription = "Rave AI Logo",
|
||||||
fontWeight = FontWeight.W900,
|
modifier = Modifier
|
||||||
color = AppColors.text
|
.height(44.dp)
|
||||||
|
.padding(top =9.dp,bottom=9.dp)
|
||||||
|
.wrapContentSize(),
|
||||||
|
// colorFilter = ColorFilter.tint(AppColors.text)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
@@ -158,7 +161,8 @@ fun Agent() {
|
|||||||
painter = painterResource(id = R.drawable.rider_pro_nav_search),
|
painter = painterResource(id = R.drawable.rider_pro_nav_search),
|
||||||
contentDescription = "search",
|
contentDescription = "search",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(24.dp)
|
.size(44.dp)
|
||||||
|
.padding(top = 9.dp,bottom=9.dp)
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
navController.navigate(NavigationRoute.Search.route)
|
navController.navigate(NavigationRoute.Search.route)
|
||||||
},
|
},
|
||||||
@@ -167,7 +171,11 @@ fun Agent() {
|
|||||||
},
|
},
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
containerColor = AppColors.background
|
containerColor = AppColors.background
|
||||||
)
|
),
|
||||||
|
windowInsets = WindowInsets(0, 0, 0, 0),
|
||||||
|
modifier = Modifier
|
||||||
|
.height(44.dp + statusBarPaddingValues.calculateTopPadding())
|
||||||
|
.padding(top = statusBarPaddingValues.calculateTopPadding())
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
containerColor = AppColors.background,
|
containerColor = AppColors.background,
|
||||||
@@ -180,8 +188,8 @@ fun Agent() {
|
|||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
.padding(
|
.padding(
|
||||||
bottom = navigationBarPaddings,
|
bottom = navigationBarPaddings,
|
||||||
start = 16.dp,
|
start = 8.dp,
|
||||||
end = 16.dp
|
end = 8.dp
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@@ -217,7 +225,7 @@ fun Agent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 动态添加分类标签
|
// 动态添加分类标签
|
||||||
viewModel.categories.take(4).forEachIndexed { index, category ->
|
viewModel.categories.forEachIndexed { index, category ->
|
||||||
item {
|
item {
|
||||||
CustomTabItem(
|
CustomTabItem(
|
||||||
text = category.name,
|
text = category.name,
|
||||||
@@ -233,58 +241,6 @@ fun Agent() {
|
|||||||
TabSpacer()
|
TabSpacer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
item {
|
|
||||||
CustomTabItem(
|
|
||||||
text = "scenes",
|
|
||||||
isSelected = selectedTabIndex == 1,
|
|
||||||
onClick = {
|
|
||||||
selectedTabIndex = 1
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
TabSpacer()
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
CustomTabItem(
|
|
||||||
text = "voices",
|
|
||||||
isSelected = selectedTabIndex == 6,
|
|
||||||
onClick = {
|
|
||||||
selectedTabIndex = 6
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
TabSpacer()
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
CustomTabItem(
|
|
||||||
text = "anime",
|
|
||||||
isSelected = selectedTabIndex == 7,
|
|
||||||
onClick = {
|
|
||||||
selectedTabIndex = 7
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
TabSpacer()
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
CustomTabItem(
|
|
||||||
text = "assist",
|
|
||||||
isSelected = selectedTabIndex == 8,
|
|
||||||
onClick = {
|
|
||||||
selectedTabIndex = 8
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,15 @@ import com.aiosman.ravenow.ui.index.tabs.message.MessageListViewModel.userServic
|
|||||||
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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存数据结构,用于存储每个分类的Agent列表
|
||||||
|
*/
|
||||||
|
data class AgentCacheData(
|
||||||
|
val items: List<AgentItem>,
|
||||||
|
val currentPage: Int,
|
||||||
|
val hasMoreData: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
object AgentViewModel: ViewModel() {
|
object AgentViewModel: ViewModel() {
|
||||||
|
|
||||||
private val apiClient: RaveNowAPI = ApiClient.api
|
private val apiClient: RaveNowAPI = ApiClient.api
|
||||||
@@ -51,13 +60,30 @@ object AgentViewModel: ViewModel() {
|
|||||||
private val pageSize = 20
|
private val pageSize = 20
|
||||||
private var currentCategoryId: Int? = null
|
private var currentCategoryId: Int? = null
|
||||||
|
|
||||||
|
// 缓存:使用分类ID作为key,null表示推荐列表
|
||||||
|
private val agentCache = mutableMapOf<Int?, AgentCacheData>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
loadAgentData()
|
loadAgentData()
|
||||||
loadCategories()
|
loadCategories()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadAgentData(categoryId: Int? = null, page: Int = 1, isLoadMore: Boolean = false) {
|
private fun loadAgentData(categoryId: Int? = null, page: Int = 1, isLoadMore: Boolean = false, forceRefresh: Boolean = false) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
// 如果不是强制刷新且不是加载更多,检查缓存
|
||||||
|
if (!forceRefresh && !isLoadMore) {
|
||||||
|
val cached = agentCache[categoryId]
|
||||||
|
if (cached != null && cached.items.isNotEmpty()) {
|
||||||
|
// 使用缓存数据
|
||||||
|
agentItems = cached.items
|
||||||
|
currentPage = cached.currentPage
|
||||||
|
hasMoreData = cached.hasMoreData
|
||||||
|
currentCategoryId = categoryId
|
||||||
|
println("使用缓存数据,分类ID: $categoryId, 数据数量: ${cached.items.size}")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isLoadMore) {
|
if (isLoadMore) {
|
||||||
isLoadingMore = true
|
isLoadingMore = true
|
||||||
} else {
|
} else {
|
||||||
@@ -76,11 +102,18 @@ object AgentViewModel: ViewModel() {
|
|||||||
page = page,
|
page = page,
|
||||||
pageSize = pageSize,
|
pageSize = pageSize,
|
||||||
withWorkflow = 1,
|
withWorkflow = 1,
|
||||||
categoryIds = listOf(categoryId)
|
categoryIds = listOf(categoryId),
|
||||||
|
random = 1
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// 获取所有智能体
|
// 获取推荐智能体,使用random=1
|
||||||
apiClient.getAgent(page = page, pageSize = pageSize, withWorkflow = 1)
|
apiClient.getAgent(
|
||||||
|
page = page,
|
||||||
|
pageSize = pageSize,
|
||||||
|
withWorkflow = 1,
|
||||||
|
categoryIds = null,
|
||||||
|
random = 1
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
@@ -103,6 +136,14 @@ object AgentViewModel: ViewModel() {
|
|||||||
// 检查是否还有更多数据
|
// 检查是否还有更多数据
|
||||||
hasMoreData = agents.size >= pageSize
|
hasMoreData = agents.size >= pageSize
|
||||||
|
|
||||||
|
// 更新缓存
|
||||||
|
agentCache[categoryId] = AgentCacheData(
|
||||||
|
items = agentItems,
|
||||||
|
currentPage = currentPage,
|
||||||
|
hasMoreData = hasMoreData
|
||||||
|
)
|
||||||
|
println("更新缓存,分类ID: $categoryId, 数据数量: ${agentItems.size}")
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
errorMessage = "获取Agent数据失败: ${response.code()}"
|
errorMessage = "获取Agent数据失败: ${response.code()}"
|
||||||
}
|
}
|
||||||
@@ -120,20 +161,32 @@ object AgentViewModel: ViewModel() {
|
|||||||
|
|
||||||
private fun loadCategories() {
|
private fun loadCategories() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
// 如果分类已经加载,不重复请求
|
||||||
|
if (categories.isNotEmpty()) {
|
||||||
|
println("使用已缓存的分类数据,数量: ${categories.size}")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 获取完整的语言标记(如 "zh-CN")
|
||||||
|
val sysLang = com.aiosman.ravenow.utils.Utils.getPreferredLanguageTag()
|
||||||
val response = apiClient.getCategories(
|
val response = apiClient.getCategories(
|
||||||
pageSize = 20,
|
page = 1,
|
||||||
|
pageSize = 100,
|
||||||
|
isActive = true,
|
||||||
withChildren = false,
|
withChildren = false,
|
||||||
withParent = false,
|
withParent = false,
|
||||||
withCount = true,
|
withCount = true,
|
||||||
hideEmpty = true
|
hideEmpty = true,
|
||||||
|
lang = sysLang
|
||||||
)
|
)
|
||||||
println("分类数据请求完成,响应成功: ${response.isSuccessful}")
|
println("分类数据请求完成,响应成功: ${response.isSuccessful}, 语言标记: $sysLang")
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
val categoryList = response.body()?.data?.list ?: emptyList()
|
val categoryList = response.body()?.list ?: emptyList()
|
||||||
println("获取到 ${categoryList.size} 个分类")
|
println("获取到 ${categoryList.size} 个分类")
|
||||||
|
// 使用当前语言获取翻译后的分类名称
|
||||||
categories = categoryList.map { category ->
|
categories = categoryList.map { category ->
|
||||||
CategoryItem.fromCategoryTemplate(category)
|
CategoryItem.fromCategoryTemplate(category, sysLang)
|
||||||
}
|
}
|
||||||
println("成功处理并映射了 ${categories.size} 个分类")
|
println("成功处理并映射了 ${categories.size} 个分类")
|
||||||
} else {
|
} else {
|
||||||
@@ -210,10 +263,12 @@ object AgentViewModel: ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 刷新推荐Agent数据
|
* 刷新当前分类的Agent数据(强制刷新,清除缓存)
|
||||||
*/
|
*/
|
||||||
fun refreshAgentData() {
|
fun refreshAgentData() {
|
||||||
loadAgentData()
|
// 清除当前分类的缓存
|
||||||
|
agentCache.remove(currentCategoryId)
|
||||||
|
loadAgentData(categoryId = currentCategoryId, forceRefresh = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -230,6 +285,7 @@ object AgentViewModel: ViewModel() {
|
|||||||
*/
|
*/
|
||||||
fun ResetModel() {
|
fun ResetModel() {
|
||||||
agentItems = emptyList()
|
agentItems = emptyList()
|
||||||
|
categories = emptyList()
|
||||||
errorMessage = null
|
errorMessage = null
|
||||||
isRefreshing = false
|
isRefreshing = false
|
||||||
isLoading = false
|
isLoading = false
|
||||||
@@ -237,6 +293,8 @@ object AgentViewModel: ViewModel() {
|
|||||||
currentPage = 1
|
currentPage = 1
|
||||||
hasMoreData = true
|
hasMoreData = true
|
||||||
currentCategoryId = null
|
currentCategoryId = null
|
||||||
|
// 清空缓存
|
||||||
|
agentCache.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -248,11 +306,11 @@ data class CategoryItem(
|
|||||||
val promptCount: Int?
|
val promptCount: Int?
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
fun fromCategoryTemplate(template: CategoryTemplate): CategoryItem {
|
fun fromCategoryTemplate(template: CategoryTemplate, lang: String): CategoryItem {
|
||||||
return CategoryItem(
|
return CategoryItem(
|
||||||
id = template.id,
|
id = template.id,
|
||||||
name = template.name,
|
name = template.getLocalizedName(lang),
|
||||||
description = template.description,
|
description = template.getLocalizedDescription(lang),
|
||||||
avatar = "${ApiClient.BASE_API_URL}${template.avatar}",
|
avatar = "${ApiClient.BASE_API_URL}${template.avatar}",
|
||||||
promptCount = template.promptCount
|
promptCount = template.promptCount
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -63,6 +63,23 @@ object Utils {
|
|||||||
return Locale.getDefault().language
|
return Locale.getDefault().language
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取完整的语言标记,如 "zh-CN", "en-US"
|
||||||
|
* 优先使用完整的 BCP-47 语言标记,提升与后端 translations 键的匹配率
|
||||||
|
*/
|
||||||
|
fun getPreferredLanguageTag(): String {
|
||||||
|
val locale = Locale.getDefault()
|
||||||
|
val language = locale.language
|
||||||
|
val country = locale.country
|
||||||
|
|
||||||
|
// 如果有国家/地区代码,返回完整的语言标记
|
||||||
|
return if (country.isNotEmpty()) {
|
||||||
|
"$language-$country"
|
||||||
|
} else {
|
||||||
|
language
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun compressImage(context: Context, uri: Uri, maxSize: Int = 512, quality: Int = 85): File {
|
fun compressImage(context: Context, uri: Uri, maxSize: Int = 512, quality: Int = 85): File {
|
||||||
val inputStream = context.contentResolver.openInputStream(uri)
|
val inputStream = context.contentResolver.openInputStream(uri)
|
||||||
val originalBitmap = BitmapFactory.decodeStream(inputStream)
|
val originalBitmap = BitmapFactory.decodeStream(inputStream)
|
||||||
|
|||||||
22
app/src/main/res/drawable/home_logo.xml
Normal file
22
app/src/main/res/drawable/home_logo.xml
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 123 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 31 KiB |
Reference in New Issue
Block a user