Explorar el Código

feat(chat): 实现聊天界面功能增强

- 添加继续命令到离线命令列表
- 将聊天Activity重构为列表布局支持多条消息显示
- 集成Markwon库实现Markdown格式内容渲染
- 实现聊天消息数据模型和列表适配器
- 添加滚动到最新消息功能
- 实现循环问题轮询机制支持连续对话
- 优化SSE流处理和异常处理逻辑
- 更新应用基础URL配置
- 移除旧的单消息布局改为RecyclerView列表布局
- 添加聊天项点击触发新问题功能
徐勤民 hace 1 día
padre
commit
4e9f609c5b

+ 2 - 2
app/src/main/java/com/nova/brain/glass/MyApplication.java

@@ -13,8 +13,8 @@ import com.xuqm.base.di.manager.HttpManager;
  */
 public class MyApplication extends App {
 
-    public static String baseUrl = "http://192.168.6.20";
-//    public static String baseUrl = "http://22fs132201.imwork.net";
+//    public static String baseUrl = "http://192.168.6.20";
+    public static String baseUrl = "http://22fs132201.imwork.net";
 
     @Override
     public void onCreate() {

+ 1 - 0
app/src/main/java/com/nova/brain/glass/helper/OfflineCmdServiceHelper.kt

@@ -15,6 +15,7 @@ object OfflineCmdServiceHelper {
 
     // 所有页面通用关键词(常量,只分配一次)
     private val COMMON_CMDS = listOf(
+        OfflineCmdBean("继续", "ji xu"),
         OfflineCmdBean("退出", "tui chu"),
         OfflineCmdBean("返回", "fan hui")
     )

+ 26 - 27
app/src/main/java/com/nova/brain/glass/ui/ChatActivity.kt

@@ -1,47 +1,56 @@
 package com.nova.brain.glass.ui
 
 import android.widget.TextView
-import androidx.lifecycle.ViewModelProvider
 import com.nova.brain.glass.R
 import com.nova.brain.glass.databinding.ActivityChatBinding
 import com.nova.brain.glass.helper.OfflineCmdListener
 import com.nova.brain.glass.helper.OfflineCmdServiceHelper
 import com.nova.brain.glass.model.ChatItem
 import com.nova.brain.glass.viewmodel.ChatVM
-import com.nova.brain.glass.viewmodel.WelcomeVM
 import com.xuqm.base.adapter.CommonPagedAdapter
 import com.xuqm.base.adapter.ViewHolder
-import com.xuqm.base.ui.BaseActivity
+import com.xuqm.base.ui.BaseListFormLayoutNormalActivity
 import io.noties.markwon.Markwon
 
-class ChatActivity : BaseActivity<ActivityChatBinding>() {
+class ChatActivity : BaseListFormLayoutNormalActivity<ChatItem, ChatVM, ActivityChatBinding>() {
     override fun getLayoutId(): Int = R.layout.activity_chat
     override fun fullscreen(): Boolean = true
+
+    private val markwon: Markwon by lazy { Markwon.create(this) }
+
     private val listener = object : OfflineCmdListener {
         override fun onOfflineCmd(cmd: String) {
             runOnUiThread {
                 when (cmd) {
-                    "退出", "返回", "退回" -> {
-                        finish()
-                    }
+                    "退出", "返回", "退回" -> finish()
+                    "继续" -> viewModel.demoPostSse()
                 }
             }
         }
     }
-    private lateinit var markwon: Markwon
-    private lateinit var vm: ChatVM
+
     override fun initData() {
         super.initData()
 
-        vm = ViewModelProvider(this)[ChatVM::class.java]
-        markwon = Markwon.create(this)
-
-        binding.title.text = "我的代办任务有哪些?"
-        vm.result.observe( this){
-            binding.content.text = it
+        viewModel.result.observe(this) {
+            val lastIndex = (recyclerView.adapter?.itemCount ?: 1) - 1
+            if (lastIndex >= 0) recyclerView.scrollToPosition(lastIndex)
         }
 
-        vm.demoPostSse()
+        viewModel.demoPostSse()
+    }
+
+    override fun adapter() = object : CommonPagedAdapter<ChatItem>(R.layout.item_chat) {
+        override fun convert(holder: ViewHolder, item: ChatItem, position: Int) {
+            val chatItems = viewModel.chatItems
+            val liveItem = if (position < chatItems.size) chatItems[position] else item
+            holder.setText(R.id.title, liveItem.title)
+            val tv = holder.getView<TextView>(R.id.content)
+            markwon.setMarkdown(tv, liveItem.content)
+            holder.setClickListener(R.id.root, {
+                viewModel.demoPostSse()
+            })
+        }
     }
 
     override fun onResume() {
@@ -53,14 +62,4 @@ class ChatActivity : BaseActivity<ActivityChatBinding>() {
         super.onPause()
         OfflineCmdServiceHelper.removeOnLineListener(listener)
     }
-
-//    private val adapter = object : CommonPagedAdapter<ChatItem>(R.layout.item_chat) {
-//        override fun convert(holder: ViewHolder, item: ChatItem, position: Int) {
-//            holder.setText(R.id.title, item.title)
-//
-//            val tv = holder.getView<TextView>(R.id.content)
-//            markwon.setMarkdown(tv, item.content);
-//        }
-//    }
-
-}
+}

+ 72 - 18
app/src/main/java/com/nova/brain/glass/viewmodel/ChatVM.kt

@@ -1,6 +1,7 @@
 package com.nova.brain.glass.viewmodel
 
 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
@@ -8,45 +9,97 @@ import com.nova.brain.glass.model.data.ChatData
 import com.nova.brain.glass.repository.Service
 import com.xuqm.base.common.GsonImplHelp
 import com.xuqm.base.di.manager.HttpManager
-import com.xuqm.sdhbwfu.core.viewModel.BaseViewModel
+import com.xuqm.base.viewmodel.BaseListViewModel
+import com.xuqm.base.viewmodel.callback.Response
+import android.os.Handler
+import android.os.Looper
+import io.reactivex.disposables.Disposable
 import io.reactivex.schedulers.Schedulers
 
-
-class ChatVM : BaseViewModel() {
+class ChatVM : BaseListViewModel<ChatItem>() {
     val result = MutableLiveData<String>()
-    private var t = "string"
+    val chatItems: MutableList<ChatItem> = mutableListOf()
+
+    private var currentTask: Disposable? = null
+
+    private val questions = listOf(
+        "我的代办任务有哪些?",
+        "今天天气情况如何?",
+        "当前设备状态如何?",
+        "今日巡检计划是什么?",
+        "有哪些待处理的告警?",
+        "最近的维修记录有哪些?",
+        "当前区域的安全评分是多少?",
+        "有哪些设备需要维护?",
+        "今天有哪些会议安排?",
+        "当前库存状态如何?"
+    )
+    private var questionIndex = 0
+    private var dataSourceReady = false
+    private val mainHandler = Handler(Looper.getMainLooper())
+
+    override fun loadData(page: Int, onResponse: Response<ChatItem>) {
+        dataSourceReady = true
+        if (page == 0) {
+            onResponse.onResponse(ArrayList(chatItems))
+        } else {
+            onResponse.onResponse(ArrayList())
+        }
+    }
+
     fun demoPostSse() {
-        t = ""
-        HttpManager.getApi(Service::class.java).chat(ChatData("我的代办任务有哪些?"))
+        currentTask?.dispose()
+        currentTask = null
+
+        val question = questions[questionIndex % questions.size]
+        questionIndex++
+
+        chatItems.add(ChatItem(question, ""))
+        if (dataSourceReady) invalidate()
+
+        currentTask = HttpManager.getApi(Service::class.java).chat(ChatData(question))
             .subscribeOn(Schedulers.io())
             .subscribe({ body ->
-                var sb = ""
+                var content = ""
+                var type = ""
                 body.charStream().buffered().use { reader ->
                     try {
                         var line: String?
                         while (reader.readLine().also { line = it } != null) {
                             val l = line!!
                             if (l.isNotEmpty()) {
+                                if (l.trimStart().startsWith("<")) {
+                                    // HTML响应(隧道断开),回退问题索引并在主线程重试
+                                    mainHandler.post {
+                                        questionIndex--
+                                        if (chatItems.isNotEmpty()) chatItems.removeAt(chatItems.size - 1)
+                                        demoPostSse()
+                                    }
+                                    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)
                                     return@use
                                 }
-                                if (t != model.type) {
-                                    sb = ""
+                                if (type != model.type) {
+                                    content = ""
                                 }
-                                t = model.type
+                                type = model.type
                                 if (model.type == "string") {
-                                    val model1 =
-                                        GsonImplHelp.get().toObject(json, ChatModel1::class.java)
-                                    sb+=model1.data
+                                    val model1 = GsonImplHelp.get().toObject(json, ChatModel1::class.java)
+                                    content += model1.data
                                 } else {
-                                    val model2 =
-                                        GsonImplHelp.get().toObject(json, ChatModel2::class.java)
-                                    sb+=model2.data.content
+                                    val model2 = GsonImplHelp.get().toObject(json, ChatModel2::class.java)
+                                    content += model2.data.content
+                                }
+                                val lastIndex = chatItems.size - 1
+                                if (lastIndex >= 0) {
+                                    chatItems[lastIndex] = chatItems[lastIndex].copy(content = content)
+                                    notifyItem(lastIndex)
                                 }
-                                result.postValue(sb)
+                                result.postValue(content)
                             }
                         }
                     } catch (e: Exception) {
@@ -55,6 +108,7 @@ class ChatVM : BaseViewModel() {
                 }
             }, { e ->
                 result.postValue("AI反馈异常: ${e.message}")
-            }).adds()
+            })
+        currentTask?.also { add(it) }
     }
 }

+ 8 - 19
app/src/main/res/layout/activity_chat.xml

@@ -1,26 +1,15 @@
 <?xml version="1.0" encoding="utf-8"?>
 <layout>
-    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:paddingHorizontal="29dp"
-        android:background="@color/app_color_black"
-        android:paddingVertical="10dp"
-        android:orientation="vertical">
+        android:background="@color/app_color_black">
 
-        <TextView
-            android:id="@+id/title"
+        <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/baseRecyclerView"
             android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:textColor="#2EB242"
-            android:textSize="10sp" />
+            android:layout_height="match_parent"
+            android:overScrollMode="never" />
 
-        <TextView
-            android:id="@+id/content"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="10dp"
-            android:textColor="#ff40FF5E"
-            android:textSize="14sp" />
-    </LinearLayout>
-</layout>
+    </FrameLayout>
+</layout>

+ 1 - 0
app/src/main/res/layout/item_chat.xml

@@ -3,6 +3,7 @@
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:layout_marginBottom="30dp"
+    android:id="@+id/root"
     android:paddingHorizontal="29dp"
     android:paddingVertical="10dp"
     android:orientation="vertical">