feat(chat): 实现聊天界面功能增强
- 添加继续命令到离线命令列表 - 将聊天Activity重构为列表布局支持多条消息显示 - 集成Markwon库实现Markdown格式内容渲染 - 实现聊天消息数据模型和列表适配器 - 添加滚动到最新消息功能 - 实现循环问题轮询机制支持连续对话 - 优化SSE流处理和异常处理逻辑 - 更新应用基础URL配置 - 移除旧的单消息布局改为RecyclerView列表布局 - 添加聊天项点击触发新问题功能
这个提交包含在:
父节点
e73d7bd15e
当前提交
4e9f609c5b
@ -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">
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户