Explorar el Código

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

- 新增 RecognizeData 和 RecognizeModel 数据类用于语音识别
- 在 Service 中添加 recognize 接口用于意图识别
- 为 ChatItem 添加 id 字段并在 ViewModel 中初始化
- 添加加载状态指示器和进度条显示
- 优化聊天列表布局,添加分割线和提示文字
- 更新加载动画颜色为主题绿色 #ff40FF5E
- 简化 ChatModel 数据结构并优化消息处理逻辑
- 添加 loading 状态管理来控制进度条显示
徐勤民 hace 1 día
padre
commit
877455a727

+ 5 - 2
app/src/main/java/com/nova/brain/glass/model/ChatItem.kt

@@ -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)
+    }
+}

+ 3 - 30
app/src/main/java/com/nova/brain/glass/model/ChatModel.kt

@@ -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,
-)
+)

+ 47 - 0
app/src/main/java/com/nova/brain/glass/model/RecognizeModel.kt

@@ -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,
+)

+ 16 - 0
app/src/main/java/com/nova/brain/glass/model/data/RecognizeData.kt

@@ -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()
+)

+ 5 - 0
app/src/main/java/com/nova/brain/glass/repository/Service.kt

@@ -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>

+ 7 - 0
app/src/main/java/com/nova/brain/glass/ui/ChatActivity.kt

@@ -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)

+ 27 - 17
app/src/main/java/com/nova/brain/glass/viewmodel/ChatVM.kt

@@ -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) }

+ 13 - 0
app/src/main/res/drawable/bg_chat.xml

@@ -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>

+ 41 - 4
app/src/main/res/layout/activity_chat.xml

@@ -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>

+ 8 - 2
app/src/main/res/layout/item_chat.xml

@@ -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"

+ 12 - 12
base/src/main/res/drawable/ic_loading.xml

@@ -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>