feat(chat): 添加语音识别功能并优化聊天界面
- 新增 RecognizeData 和 RecognizeModel 数据类用于语音识别 - 在 Service 中添加 recognize 接口用于意图识别 - 为 ChatItem 添加 id 字段并在 ViewModel 中初始化 - 添加加载状态指示器和进度条显示 - 优化聊天列表布局,添加分割线和提示文字 - 更新加载动画颜色为主题绿色 #ff40FF5E - 简化 ChatModel 数据结构并优化消息处理逻辑 - 添加 loading 状态管理来控制进度条显示
这个提交包含在:
父节点
4e9f609c5b
当前提交
877455a727
@ -2,5 +2,8 @@ package com.nova.brain.glass.model
|
||||
|
||||
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
|
||||
|
||||
//{
|
||||
// "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}
|
||||
data class ChatModel(
|
||||
val role: String?,
|
||||
val type: String?,
|
||||
val msg: String?,
|
||||
)
|
||||
|
||||
// {"id":74316,"role":"assistant","createTime":"2026-04-16T08:28:06.428Z","type":"string","data":"建议使用","metadata":{}}
|
||||
data class ChatModel1(
|
||||
val id: Int,
|
||||
val role: String,
|
||||
val createTime: String,
|
||||
val type: 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
|
||||
|
||||
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.RecognizeData
|
||||
import io.reactivex.Observable
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.ResponseBody
|
||||
@ -19,6 +21,9 @@ interface Service {
|
||||
@POST("/cbrain-gateway/cbrain-task-server/cbrain-task/task/glassesSearch")
|
||||
fun demoPost(@Body body: RequestBody): Observable<ResponseBody>
|
||||
|
||||
@POST("/api/intent/recognize")
|
||||
fun recognize(@Body body: RecognizeData): Observable<RecognizeModel>
|
||||
|
||||
@Streaming
|
||||
@POST("/cbrain-gateway/cbraindep/docqa/chat/qa03")
|
||||
fun chat(@Body body: ChatData): Observable<ResponseBody>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package com.nova.brain.glass.ui
|
||||
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import com.nova.brain.glass.R
|
||||
import com.nova.brain.glass.databinding.ActivityChatBinding
|
||||
@ -37,11 +38,17 @@ class ChatActivity : BaseListFormLayoutNormalActivity<ChatItem, ChatVM, Activity
|
||||
if (lastIndex >= 0) recyclerView.scrollToPosition(lastIndex)
|
||||
}
|
||||
|
||||
viewModel.loading.observe(this) { loading ->
|
||||
binding.pb.visibility = if (loading) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
viewModel.demoPostSse()
|
||||
|
||||
}
|
||||
|
||||
override fun adapter() = object : CommonPagedAdapter<ChatItem>(R.layout.item_chat) {
|
||||
override fun convert(holder: ViewHolder, item: ChatItem, position: Int) {
|
||||
holder.setVisibility(R.id.line, position!=0)
|
||||
val chatItems = viewModel.chatItems
|
||||
val liveItem = if (position < chatItems.size) chatItems[position] else item
|
||||
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.ChatModel
|
||||
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.repository.Service
|
||||
import com.xuqm.base.common.GsonImplHelp
|
||||
@ -18,6 +17,7 @@ import io.reactivex.schedulers.Schedulers
|
||||
|
||||
class ChatVM : BaseListViewModel<ChatItem>() {
|
||||
val result = MutableLiveData<String>()
|
||||
val loading = MutableLiveData<Boolean>()
|
||||
val chatItems: MutableList<ChatItem> = mutableListOf()
|
||||
|
||||
private var currentTask: Disposable? = null
|
||||
@ -52,16 +52,17 @@ class ChatVM : BaseListViewModel<ChatItem>() {
|
||||
currentTask = null
|
||||
|
||||
val question = questions[questionIndex % questions.size]
|
||||
val itemId = questionIndex
|
||||
questionIndex++
|
||||
|
||||
chatItems.add(ChatItem(question, ""))
|
||||
chatItems.add(ChatItem(itemId, question, ""))
|
||||
if (dataSourceReady) invalidate()
|
||||
loading.postValue(true)
|
||||
|
||||
currentTask = HttpManager.getApi(Service::class.java).chat(ChatData(question))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe({ body ->
|
||||
var content = ""
|
||||
var type = ""
|
||||
body.charStream().buffered().use { reader ->
|
||||
try {
|
||||
var line: String?
|
||||
@ -75,38 +76,47 @@ class ChatVM : BaseListViewModel<ChatItem>() {
|
||||
if (chatItems.isNotEmpty()) chatItems.removeAt(chatItems.size - 1)
|
||||
demoPostSse()
|
||||
}
|
||||
loading.postValue(false)
|
||||
return@use
|
||||
}
|
||||
val json = if (l.startsWith("data:")) l.removePrefix("data:").trim() else l
|
||||
val model = GsonImplHelp.get().toObject(json, ChatModel::class.java)
|
||||
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
|
||||
}
|
||||
if (type != model.type) {
|
||||
content = ""
|
||||
}
|
||||
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
|
||||
}
|
||||
if (model.role != "assistant" || model.type != "string") continue
|
||||
val model1 = GsonImplHelp.get().toObject(json, ChatModel1::class.java)
|
||||
content += model1.data
|
||||
val lastIndex = chatItems.size - 1
|
||||
if (lastIndex >= 0) {
|
||||
chatItems[lastIndex] = chatItems[lastIndex].copy(content = content)
|
||||
chatItems[lastIndex].content = content
|
||||
notifyItem(lastIndex)
|
||||
}
|
||||
result.postValue(content)
|
||||
}
|
||||
}
|
||||
} 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 ->
|
||||
loading.postValue(false)
|
||||
result.postValue("AI反馈异常: ${e.message}")
|
||||
})
|
||||
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"?>
|
||||
<layout>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<layout xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/app_color_black">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/baseRecyclerView"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="400dp"
|
||||
android:background="@drawable/bg_chat"
|
||||
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>
|
||||
@ -2,19 +2,25 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="30dp"
|
||||
android:layout_marginBottom="15dp"
|
||||
android:id="@+id/root"
|
||||
android:paddingHorizontal="29dp"
|
||||
android:paddingVertical="10dp"
|
||||
android:orientation="vertical">
|
||||
<View
|
||||
android:id="@+id/line"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="#ff40FF5E"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_marginTop="15dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="驳回"
|
||||
android:textColor="#2EB242"
|
||||
android:textSize="10sp" />
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/content"
|
||||
|
||||
@ -6,39 +6,39 @@
|
||||
|
||||
<!--android:fillColor="#409CFA"-->
|
||||
<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"/>
|
||||
<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"/>
|
||||
<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"/>
|
||||
<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"/>
|
||||
<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"/>
|
||||
<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"/>
|
||||
<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"/>
|
||||
<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"/>
|
||||
<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"/>
|
||||
<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"/>
|
||||
<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"/>
|
||||
<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"/>
|
||||
</vector>
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户