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

- 添加继续命令到离线命令列表
- 将聊天Activity重构为列表布局支持多条消息显示
- 集成Markwon库实现Markdown格式内容渲染
- 实现聊天消息数据模型和列表适配器
- 添加滚动到最新消息功能
- 实现循环问题轮询机制支持连续对话
- 优化SSE流处理和异常处理逻辑
- 更新应用基础URL配置
- 移除旧的单消息布局改为RecyclerView列表布局
- 添加聊天项点击触发新问题功能
这个提交包含在:
徐勤民 2026-04-16 16:01:03 +08:00
父节点 e73d7bd15e
当前提交 4e9f609c5b
共有 6 个文件被更改,包括 110 次插入66 次删除

查看文件

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

查看文件

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

查看文件

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

查看文件

@ -1,6 +1,7 @@
package com.nova.brain.glass.viewmodel package com.nova.brain.glass.viewmodel
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
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.ChatModel2
@ -8,45 +9,97 @@ 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
import com.xuqm.base.di.manager.HttpManager 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 import io.reactivex.schedulers.Schedulers
class ChatVM : BaseListViewModel<ChatItem>() {
class ChatVM : BaseViewModel() {
val result = MutableLiveData<String>() 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() { fun demoPostSse() {
t = "" currentTask?.dispose()
HttpManager.getApi(Service::class.java).chat(ChatData("我的代办任务有哪些?")) 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()) .subscribeOn(Schedulers.io())
.subscribe({ body -> .subscribe({ body ->
var sb = "" var content = ""
var type = ""
body.charStream().buffered().use { reader -> body.charStream().buffered().use { reader ->
try { try {
var line: String? var line: String?
while (reader.readLine().also { line = it } != null) { while (reader.readLine().also { line = it } != null) {
val l = line!! val l = line!!
if (l.isNotEmpty()) { 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 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) result.postValue(model.msg ?: json)
return@use return@use
} }
if (t != model.type) { if (type != model.type) {
sb = "" content = ""
} }
t = model.type type = model.type
if (model.type == "string") { if (model.type == "string") {
val model1 = val model1 = GsonImplHelp.get().toObject(json, ChatModel1::class.java)
GsonImplHelp.get().toObject(json, ChatModel1::class.java) content += model1.data
sb+=model1.data
} else { } else {
val model2 = val model2 = GsonImplHelp.get().toObject(json, ChatModel2::class.java)
GsonImplHelp.get().toObject(json, ChatModel2::class.java) content += model2.data.content
sb+=model2.data.content
} }
result.postValue(sb) val lastIndex = chatItems.size - 1
if (lastIndex >= 0) {
chatItems[lastIndex] = chatItems[lastIndex].copy(content = content)
notifyItem(lastIndex)
}
result.postValue(content)
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
@ -55,6 +108,7 @@ class ChatVM : BaseViewModel() {
} }
}, { e -> }, { e ->
result.postValue("AI反馈异常: ${e.message}") result.postValue("AI反馈异常: ${e.message}")
}).adds() })
currentTask?.also { add(it) }
} }
} }

查看文件

@ -1,26 +1,15 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<layout> <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_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:paddingHorizontal="29dp" android:background="@color/app_color_black">
android:background="@color/app_color_black"
android:paddingVertical="10dp"
android:orientation="vertical">
<TextView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/title" android:id="@+id/baseRecyclerView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:textColor="#2EB242" android:overScrollMode="never" />
android:textSize="10sp" />
<TextView </FrameLayout>
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> </layout>

查看文件

@ -3,6 +3,7 @@
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="30dp"
android:id="@+id/root"
android:paddingHorizontal="29dp" android:paddingHorizontal="29dp"
android:paddingVertical="10dp" android:paddingVertical="10dp"
android:orientation="vertical"> android:orientation="vertical">