feat(chat): 添加语音识别功能并优化聊天界面

- 新增 RecognizeData 和 RecognizeModel 数据类用于语音识别
- 在 Service 中添加 recognize 接口用于意图识别
- 为 ChatItem 添加 id 字段并在 ViewModel 中初始化
- 添加加载状态指示器和进度条显示
- 优化聊天列表布局,添加分割线和提示文字
- 更新加载动画颜色为主题绿色 #ff40FF5E
- 简化 ChatModel 数据结构并优化消息处理逻辑
- 添加 loading 状态管理来控制进度条显示
这个提交包含在:
徐勤民 2026-04-16 16:38:52 +08:00
父节点 4e9f609c5b
当前提交 877455a727
共有 11 个文件被更改,包括 184 次插入67 次删除

查看文件

@ -2,5 +2,8 @@ package com.nova.brain.glass.model
import com.xuqm.base.adapter.BaseItem import com.xuqm.base.adapter.BaseItem
data class ChatItem(val title: String, val content: String): BaseItem() { data class ChatItem(val id: Int, val title: String, var content: String) : BaseItem() {
} init {
setS_id(id)
}
}

查看文件

@ -1,44 +1,17 @@
package com.nova.brain.glass.model package com.nova.brain.glass.model
//{
// "id": 74216,
// "role": "assistant",
// "createTime": "2026-04-16T06:13:32.678Z",
// "type": "reason",
// "data": {
// "content": "数据。",
// "duration": 0.0
// },
// "metadata": {}
//}
//{
// "id": 74216,
// "role": "assistant",
// "createTime": "2026-04-16T06:13:32.678Z",
// "type": "string",
// "data": "最",
// "metadata": {}
//}
//{"date":"2026-04-16T06:49:19.790Z","msg":"当前话题存在进行中的请求,请稍后重试","code":409,"success":false,"uri":"/docqa/chat/qa03","status":409} //{"date":"2026-04-16T06:49:19.790Z","msg":"当前话题存在进行中的请求,请稍后重试","code":409,"success":false,"uri":"/docqa/chat/qa03","status":409}
data class ChatModel( data class ChatModel(
val role: String?,
val type: String?, val type: String?,
val msg: String?, val msg: String?,
) )
// {"id":74316,"role":"assistant","createTime":"2026-04-16T08:28:06.428Z","type":"string","data":"建议使用","metadata":{}}
data class ChatModel1( data class ChatModel1(
val id: Int, val id: Int,
val role: String, val role: String,
val createTime: String, val createTime: String,
val type: String, val type: String,
val data: String, val data: String,
) )
data class ChatModel2Data(
val content: String,
)
data class ChatModel2(
val id: Int,
val role: String,
val createTime: String,
val type: String,
val data: ChatModel2Data,
)

查看文件

@ -0,0 +1,47 @@
package com.nova.brain.glass.model
//{
// "code": "0",
// "data": {
// "action": {
// "name": "goToDecisionCenter",
// "params": {
// "question": "C919第45架机的零部件采购情况"
// }
// }
// },
// "message": "home"
//}
data class RecognizeModel(
val code: String,
val data: RecognizeModelData,
val message: String
)
//{
// "action": {
// "name": "goToDecisionCenter",
// "params": {
// "question": "C919第45架机的零部件采购情况"
// }
// }
// }
data class RecognizeModelData(
val action: RecognizeAction,
)
//{
// "name": "goToDecisionCenter",
// "params": {
// "question": "C919第45架机的零部件采购情况"
// }
// }
data class RecognizeAction(
val name: String,
val params: RecognizeParams,
)
data class RecognizeParams(
val question: String,
)

查看文件

@ -0,0 +1,16 @@
package com.nova.brain.glass.model.data
//{
// "text": "查看下C919第45架机的零部件采购情况",
// "scence": "home",
// "extra": [],
// "actions": [
// "goToTaskCenter","goToDecisionCenter"
// ]
//}
data class RecognizeData(
val text: String,
val scence: String = "home",
val extra: List<String> = emptyList(),
val actions: List<String> = emptyList()
)

查看文件

@ -1,7 +1,9 @@
package com.nova.brain.glass.repository package com.nova.brain.glass.repository
import com.nova.brain.glass.model.ChatModel import com.nova.brain.glass.model.ChatModel
import com.nova.brain.glass.model.RecognizeModel
import com.nova.brain.glass.model.data.ChatData import com.nova.brain.glass.model.data.ChatData
import com.nova.brain.glass.model.data.RecognizeData
import io.reactivex.Observable import io.reactivex.Observable
import okhttp3.RequestBody import okhttp3.RequestBody
import okhttp3.ResponseBody import okhttp3.ResponseBody
@ -19,6 +21,9 @@ interface Service {
@POST("/cbrain-gateway/cbrain-task-server/cbrain-task/task/glassesSearch") @POST("/cbrain-gateway/cbrain-task-server/cbrain-task/task/glassesSearch")
fun demoPost(@Body body: RequestBody): Observable<ResponseBody> fun demoPost(@Body body: RequestBody): Observable<ResponseBody>
@POST("/api/intent/recognize")
fun recognize(@Body body: RecognizeData): Observable<RecognizeModel>
@Streaming @Streaming
@POST("/cbrain-gateway/cbraindep/docqa/chat/qa03") @POST("/cbrain-gateway/cbraindep/docqa/chat/qa03")
fun chat(@Body body: ChatData): Observable<ResponseBody> fun chat(@Body body: ChatData): Observable<ResponseBody>

查看文件

@ -1,5 +1,6 @@
package com.nova.brain.glass.ui package com.nova.brain.glass.ui
import android.view.View
import android.widget.TextView import android.widget.TextView
import com.nova.brain.glass.R import com.nova.brain.glass.R
import com.nova.brain.glass.databinding.ActivityChatBinding import com.nova.brain.glass.databinding.ActivityChatBinding
@ -37,11 +38,17 @@ class ChatActivity : BaseListFormLayoutNormalActivity<ChatItem, ChatVM, Activity
if (lastIndex >= 0) recyclerView.scrollToPosition(lastIndex) if (lastIndex >= 0) recyclerView.scrollToPosition(lastIndex)
} }
viewModel.loading.observe(this) { loading ->
binding.pb.visibility = if (loading) View.VISIBLE else View.GONE
}
viewModel.demoPostSse() viewModel.demoPostSse()
} }
override fun adapter() = object : CommonPagedAdapter<ChatItem>(R.layout.item_chat) { override fun adapter() = object : CommonPagedAdapter<ChatItem>(R.layout.item_chat) {
override fun convert(holder: ViewHolder, item: ChatItem, position: Int) { override fun convert(holder: ViewHolder, item: ChatItem, position: Int) {
holder.setVisibility(R.id.line, position!=0)
val chatItems = viewModel.chatItems val chatItems = viewModel.chatItems
val liveItem = if (position < chatItems.size) chatItems[position] else item val liveItem = if (position < chatItems.size) chatItems[position] else item
holder.setText(R.id.title, liveItem.title) holder.setText(R.id.title, liveItem.title)

查看文件

@ -4,7 +4,6 @@ import androidx.lifecycle.MutableLiveData
import com.nova.brain.glass.model.ChatItem import com.nova.brain.glass.model.ChatItem
import com.nova.brain.glass.model.ChatModel import com.nova.brain.glass.model.ChatModel
import com.nova.brain.glass.model.ChatModel1 import com.nova.brain.glass.model.ChatModel1
import com.nova.brain.glass.model.ChatModel2
import com.nova.brain.glass.model.data.ChatData import com.nova.brain.glass.model.data.ChatData
import com.nova.brain.glass.repository.Service import com.nova.brain.glass.repository.Service
import com.xuqm.base.common.GsonImplHelp import com.xuqm.base.common.GsonImplHelp
@ -18,6 +17,7 @@ import io.reactivex.schedulers.Schedulers
class ChatVM : BaseListViewModel<ChatItem>() { class ChatVM : BaseListViewModel<ChatItem>() {
val result = MutableLiveData<String>() val result = MutableLiveData<String>()
val loading = MutableLiveData<Boolean>()
val chatItems: MutableList<ChatItem> = mutableListOf() val chatItems: MutableList<ChatItem> = mutableListOf()
private var currentTask: Disposable? = null private var currentTask: Disposable? = null
@ -52,16 +52,17 @@ class ChatVM : BaseListViewModel<ChatItem>() {
currentTask = null currentTask = null
val question = questions[questionIndex % questions.size] val question = questions[questionIndex % questions.size]
val itemId = questionIndex
questionIndex++ questionIndex++
chatItems.add(ChatItem(question, "")) chatItems.add(ChatItem(itemId, question, ""))
if (dataSourceReady) invalidate() if (dataSourceReady) invalidate()
loading.postValue(true)
currentTask = HttpManager.getApi(Service::class.java).chat(ChatData(question)) currentTask = HttpManager.getApi(Service::class.java).chat(ChatData(question))
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.subscribe({ body -> .subscribe({ body ->
var content = "" var content = ""
var type = ""
body.charStream().buffered().use { reader -> body.charStream().buffered().use { reader ->
try { try {
var line: String? var line: String?
@ -75,38 +76,47 @@ class ChatVM : BaseListViewModel<ChatItem>() {
if (chatItems.isNotEmpty()) chatItems.removeAt(chatItems.size - 1) if (chatItems.isNotEmpty()) chatItems.removeAt(chatItems.size - 1)
demoPostSse() demoPostSse()
} }
loading.postValue(false)
return@use return@use
} }
val json = if (l.startsWith("data:")) l.removePrefix("data:").trim() else l val json = if (l.startsWith("data:")) l.removePrefix("data:").trim() else l
val model = GsonImplHelp.get().toObject(json, ChatModel::class.java) val model = GsonImplHelp.get().toObject(json, ChatModel::class.java)
if (model.type == null) { if (model.type == null) {
result.postValue(model.msg ?: json) loading.postValue(false)
val msg = model.msg ?: json
val lastIndex = chatItems.size - 1
if (lastIndex >= 0) {
chatItems[lastIndex].content = msg
notifyItem(lastIndex)
}
result.postValue(msg)
return@use return@use
} }
if (type != model.type) { if (model.role != "assistant" || model.type != "string") continue
content = "" val model1 = GsonImplHelp.get().toObject(json, ChatModel1::class.java)
} content += model1.data
type = model.type
if (model.type == "string") {
val model1 = GsonImplHelp.get().toObject(json, ChatModel1::class.java)
content += model1.data
} else {
val model2 = GsonImplHelp.get().toObject(json, ChatModel2::class.java)
content += model2.data.content
}
val lastIndex = chatItems.size - 1 val lastIndex = chatItems.size - 1
if (lastIndex >= 0) { if (lastIndex >= 0) {
chatItems[lastIndex] = chatItems[lastIndex].copy(content = content) chatItems[lastIndex].content = content
notifyItem(lastIndex) notifyItem(lastIndex)
} }
result.postValue(content) result.postValue(content)
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
result.postValue("AI反馈异常: ${e.message}") loading.postValue(false)
val errMsg = "AI反馈异常: ${e.message}"
val lastIndex = chatItems.size - 1
if (lastIndex >= 0) {
chatItems[lastIndex].content = errMsg
notifyItem(lastIndex)
}
result.postValue(errMsg)
} }
} }
loading.postValue(false)
}, { e -> }, { e ->
loading.postValue(false)
result.postValue("AI反馈异常: ${e.message}") result.postValue("AI反馈异常: ${e.message}")
}) })
currentTask?.also { add(it) } currentTask?.also { add(it) }

查看文件

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<stroke
android:width="2dp"
android:color="#ff40FF5E" />
<corners android:radius="4dp" />
<padding
android:bottom="5dp"
android:left="10dp"
android:right="10dp"
android:top="5dp" />
</shape>

查看文件

@ -1,15 +1,52 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<layout> <layout xmlns:app="http://schemas.android.com/apk/res-auto">
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@color/app_color_black"> android:background="@color/app_color_black">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/baseRecyclerView" android:id="@+id/baseRecyclerView"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="400dp"
android:background="@drawable/bg_chat"
android:overScrollMode="never" /> android:overScrollMode="never" />
<ProgressBar
android:id="@+id/pb"
app:layout_constraintTop_toBottomOf="@+id/baseRecyclerView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="@+id/baseRecyclerView"
android:layout_width="50dp"
android:layout_height="50dp"
android:indeterminateDrawable="@drawable/load_progress" />
</FrameLayout> <TextView
android:id="@+id/hint1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:gravity="center"
android:text='滚动翻页'
android:textColor="#ff40FF5E"
android:textSize="14sp"
app:layout_constraintEnd_toStartOf="@+id/hint2"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/baseRecyclerView" />
<TextView
android:id="@+id/hint2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:gravity="center"
android:text='双击退出'
android:textColor="#ff40FF5E"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/hint1"
app:layout_constraintTop_toBottomOf="@+id/baseRecyclerView" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout> </layout>

查看文件

@ -2,19 +2,25 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="30dp" android:layout_marginBottom="15dp"
android:id="@+id/root" android:id="@+id/root"
android:paddingHorizontal="29dp" android:paddingHorizontal="29dp"
android:paddingVertical="10dp" android:paddingVertical="10dp"
android:orientation="vertical"> android:orientation="vertical">
<View
android:id="@+id/line"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#ff40FF5E"/>
<TextView <TextView
android:id="@+id/title" android:id="@+id/title"
android:layout_marginTop="15dp"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="驳回" android:text="驳回"
android:textColor="#2EB242" android:textColor="#2EB242"
android:textSize="10sp" /> android:textSize="14sp" />
<TextView <TextView
android:id="@+id/content" android:id="@+id/content"

查看文件

@ -6,39 +6,39 @@
<!--android:fillColor="#409CFA"--> <!--android:fillColor="#409CFA"-->
<path <path
android:fillColor="@color/text_black" android:fillColor="#ff40FF5E"
android:pathData="M523.09,101.85m-101.85,0a101.85,101.85 0,1 0,203.7 0,101.85 101.85,0 1,0 -203.7,0Z"/> android:pathData="M523.09,101.85m-101.85,0a101.85,101.85 0,1 0,203.7 0,101.85 101.85,0 1,0 -203.7,0Z"/>
<path <path
android:fillColor="@color/text_black" android:fillColor="#ff40FF5E"
android:pathData="M769.84,187.51m-96.03,0a96.03,96.03 0,1 0,192.06 0,96.03 96.03,0 1,0 -192.06,0Z"/> android:pathData="M769.84,187.51m-96.03,0a96.03,96.03 0,1 0,192.06 0,96.03 96.03,0 1,0 -192.06,0Z"/>
<path <path
android:fillColor="@color/text_black" android:fillColor="#ff40FF5E"
android:pathData="M903.29,381.4m-90.21,0a90.21,90.21 0,1 0,180.42 0,90.21 90.21,0 1,0 -180.42,0Z"/> android:pathData="M903.29,381.4m-90.21,0a90.21,90.21 0,1 0,180.42 0,90.21 90.21,0 1,0 -180.42,0Z"/>
<path <path
android:fillColor="@color/text_black" android:fillColor="#ff40FF5E"
android:pathData="M905.95,609.72m-84.39,0a84.39,84.39 0,1 0,168.78 0,84.39 84.39,0 1,0 -168.78,0Z"/> android:pathData="M905.95,609.72m-84.39,0a84.39,84.39 0,1 0,168.78 0,84.39 84.39,0 1,0 -168.78,0Z"/>
<path <path
android:fillColor="@color/text_black" android:fillColor="#ff40FF5E"
android:pathData="M800,786.13m-78.57,0a78.57,78.57 0,1 0,157.14 0,78.57 78.57,0 1,0 -157.14,0Z"/> android:pathData="M800,786.13m-78.57,0a78.57,78.57 0,1 0,157.14 0,78.57 78.57,0 1,0 -157.14,0Z"/>
<path <path
android:fillColor="@color/text_black" android:fillColor="#ff40FF5E"
android:pathData="M605.2,889.71m-72.75,0a72.75,72.75 0,1 0,145.5 0,72.75 72.75,0 1,0 -145.5,0Z"/> android:pathData="M605.2,889.71m-72.75,0a72.75,72.75 0,1 0,145.5 0,72.75 72.75,0 1,0 -145.5,0Z"/>
<path <path
android:fillColor="@color/text_black" android:fillColor="#ff40FF5E"
android:pathData="M397.15,877.86m-66.93,0a66.93,66.93 0,1 0,133.86 0,66.93 66.93,0 1,0 -133.86,0Z"/> android:pathData="M397.15,877.86m-66.93,0a66.93,66.93 0,1 0,133.86 0,66.93 66.93,0 1,0 -133.86,0Z"/>
<path <path
android:fillColor="@color/text_black" android:fillColor="#ff40FF5E"
android:pathData="M223.67,762.48m-61.11,0a61.11,61.11 0,1 0,122.22 0,61.11 61.11,0 1,0 -122.22,0Z"/> android:pathData="M223.67,762.48m-61.11,0a61.11,61.11 0,1 0,122.22 0,61.11 61.11,0 1,0 -122.22,0Z"/>
<path <path
android:fillColor="@color/text_black" android:fillColor="#ff40FF5E"
android:pathData="M134.48,587.15m-55.29,0a55.29,55.29 0,1 0,110.58 0,55.29 55.29,0 1,0 -110.58,0Z"/> android:pathData="M134.48,587.15m-55.29,0a55.29,55.29 0,1 0,110.58 0,55.29 55.29,0 1,0 -110.58,0Z"/>
<path <path
android:fillColor="@color/text_black" android:fillColor="#ff40FF5E"
android:pathData="M135.4,408.9m-49.47,0a49.47,49.47 0,1 0,98.94 0,49.47 49.47,0 1,0 -98.94,0Z"/> android:pathData="M135.4,408.9m-49.47,0a49.47,49.47 0,1 0,98.94 0,49.47 49.47,0 1,0 -98.94,0Z"/>
<path <path
android:fillColor="@color/text_black" android:fillColor="#ff40FF5E"
android:pathData="M205.34,260.05m-43.65,0a43.65,43.65 0,1 0,87.3 0,43.65 43.65,0 1,0 -87.3,0Z"/> android:pathData="M205.34,260.05m-43.65,0a43.65,43.65 0,1 0,87.3 0,43.65 43.65,0 1,0 -87.3,0Z"/>
<path <path
android:fillColor="@color/text_black" android:fillColor="#ff40FF5E"
android:pathData="M315.82,159.99m-37.83,0a37.83,37.83 0,1 0,75.66 0,37.83 37.83,0 1,0 -75.66,0Z"/> android:pathData="M315.82,159.99m-37.83,0a37.83,37.83 0,1 0,75.66 0,37.83 37.83,0 1,0 -75.66,0Z"/>
</vector> </vector>