fix(chat): 修复聊天界面加载状态和滚动问题
- 添加了 onComplete 回调参数确保识别完成后重置加载状态 - 修复 Toast 显示错误信息时使用正确的 message 字段 - 改进加载指示器显示逻辑,添加第二个进度条 pb1 - 实现智能滚动到底部功能,支持 SSE 流式内容更新 - 优化 recognizeAndChat 方法中的加载状态管理 - 添加 UUID 随机数触发结果更新,过滤 SSE 非数据行
这个提交包含在:
父节点
57c505d4b5
当前提交
83ce6341ab
@ -32,6 +32,7 @@ object IntentRecognizeHelper {
|
|||||||
private var disposable: Disposable? = null
|
private var disposable: Disposable? = null
|
||||||
|
|
||||||
private val baseUrl: String = "https://22v1322u01.vicp.fun"
|
private val baseUrl: String = "https://22v1322u01.vicp.fun"
|
||||||
|
// private val baseUrl: String = "http://192.168.6.20:12119"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param context 用于显示 Toast
|
* @param context 用于显示 Toast
|
||||||
@ -39,11 +40,15 @@ object IntentRecognizeHelper {
|
|||||||
* @param scence 场景标识,默认 "home"
|
* @param scence 场景标识,默认 "home"
|
||||||
* @param onSuccess 识别成功且 code=="0" 时回调,参数为 [RecognizeAction]
|
* @param onSuccess 识别成功且 code=="0" 时回调,参数为 [RecognizeAction]
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* @param onComplete 无论成功失败都会回调,用于调用方重置 loading 状态
|
||||||
|
*/
|
||||||
fun recognize(
|
fun recognize(
|
||||||
context: Context,
|
context: Context,
|
||||||
text: String? = null,
|
text: String? = null,
|
||||||
scence: String = "home",
|
scence: String = "home",
|
||||||
onSuccess: (action: RecognizeAction) -> Unit
|
onSuccess: (action: RecognizeAction) -> Unit,
|
||||||
|
onComplete: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
disposable?.dispose()
|
disposable?.dispose()
|
||||||
val question = text ?: nextQuestion()
|
val question = text ?: nextQuestion()
|
||||||
@ -59,10 +64,12 @@ object IntentRecognizeHelper {
|
|||||||
if (model.code == "0") {
|
if (model.code == "0") {
|
||||||
onSuccess(model.data.action)
|
onSuccess(model.data.action)
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(context, model.data.error, Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, model.message, Toast.LENGTH_SHORT).show()
|
||||||
|
onComplete()
|
||||||
}
|
}
|
||||||
}, { e ->
|
}, { e ->
|
||||||
Toast.makeText(context, "请求失败: ${e.message}", Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, "请求失败: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||||
|
onComplete()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -35,13 +35,17 @@ class ChatActivity : BaseListFormLayoutNormalActivity<ChatItem, ChatVM, Activity
|
|||||||
override fun initData() {
|
override fun initData() {
|
||||||
super.initData()
|
super.initData()
|
||||||
|
|
||||||
|
// SSE 流式内容更新时持续滚底
|
||||||
viewModel.result.observe(this) {
|
viewModel.result.observe(this) {
|
||||||
val lastIndex = (recyclerView.adapter?.itemCount ?: 1) - 1
|
scrollToBottom()
|
||||||
if (lastIndex >= 0) recyclerView.scrollToPosition(lastIndex)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loading=true:pb 显示旋转 + 延一帧滚底(等 PagedList 提交到 adapter)
|
||||||
|
// loading=false:pb 停转并隐藏
|
||||||
viewModel.loading.observe(this) { loading ->
|
viewModel.loading.observe(this) { loading ->
|
||||||
binding.pb.isIndeterminate = loading
|
binding.pb.visibility = if (loading) View.VISIBLE else View.INVISIBLE
|
||||||
|
binding.pb1.visibility = if (!loading) View.VISIBLE else View.INVISIBLE
|
||||||
|
recyclerView.post { scrollToBottom() }
|
||||||
}
|
}
|
||||||
|
|
||||||
val question = intent.getStringExtra("question") ?: ""
|
val question = intent.getStringExtra("question") ?: ""
|
||||||
@ -50,18 +54,47 @@ class ChatActivity : BaseListFormLayoutNormalActivity<ChatItem, ChatVM, Activity
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用意图识别,识别成功后发起 SSE 请求。
|
||||||
|
* 立即启动 pb 旋转,不等 recognize 接口响应。
|
||||||
|
*/
|
||||||
private fun recognizeAndChat() {
|
private fun recognizeAndChat() {
|
||||||
|
binding.pb.visibility = View.VISIBLE
|
||||||
|
binding.pb1.visibility = View.INVISIBLE
|
||||||
IntentRecognizeHelper.recognize(
|
IntentRecognizeHelper.recognize(
|
||||||
context = this,
|
context = this,
|
||||||
scence = "decision",
|
scence = "decision",
|
||||||
onSuccess = { action ->
|
onSuccess = { action ->
|
||||||
if (action.name == "goToDecisionCenter") {
|
if (action.name == "goToDecisionCenter") {
|
||||||
viewModel.demoPostSse(action.params.question)
|
viewModel.demoPostSse(action.params.question)
|
||||||
|
} else {
|
||||||
|
// 识别成功但没有匹配动作,停止旋转
|
||||||
|
binding.pb.isIndeterminate = false
|
||||||
|
binding.pb.visibility = View.INVISIBLE
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onComplete = {
|
||||||
|
// recognize 失败时兜底停止旋转(成功路径由 loading LiveData 接管)
|
||||||
|
binding.pb.isIndeterminate = false
|
||||||
|
binding.pb.visibility = View.INVISIBLE
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun scrollToBottom() {
|
||||||
|
val lastIndex = (recyclerView.adapter?.itemCount ?: 1) - 1
|
||||||
|
if (lastIndex < 0) return
|
||||||
|
val lm = recyclerView.layoutManager as? androidx.recyclerview.widget.LinearLayoutManager ?: return
|
||||||
|
// 第一帧:确保 lastIndex 进入可视区域
|
||||||
|
lm.scrollToPosition(lastIndex)
|
||||||
|
// 第二帧:layout 完成后,把 lastView 的底部对齐到 RecyclerView 底部
|
||||||
|
recyclerView.post {
|
||||||
|
val lastView = lm.findViewByPosition(lastIndex) ?: return@post
|
||||||
|
val gap = lastView.bottom - (recyclerView.height - recyclerView.paddingBottom)
|
||||||
|
if (gap > 0) recyclerView.scrollBy(0, gap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun adapter() = object : CommonPagedAdapter<ChatItem>(R.layout.item_chat) {
|
override fun adapter() = object : CommonPagedAdapter<ChatItem>(R.layout.item_chat) {
|
||||||
override fun convert(holder: ViewHolder, item: ChatItem, position: Int) {
|
override fun convert(holder: ViewHolder, item: ChatItem, position: Int) {
|
||||||
holder.setVisibility(R.id.line, position != 0)
|
holder.setVisibility(R.id.line, position != 0)
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import com.xuqm.base.viewmodel.BaseListViewModel
|
|||||||
import com.xuqm.base.viewmodel.callback.Response
|
import com.xuqm.base.viewmodel.callback.Response
|
||||||
import io.reactivex.disposables.Disposable
|
import io.reactivex.disposables.Disposable
|
||||||
import io.reactivex.schedulers.Schedulers
|
import io.reactivex.schedulers.Schedulers
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
class ChatVM : BaseListViewModel<ChatItem>() {
|
class ChatVM : BaseListViewModel<ChatItem>() {
|
||||||
val result = MutableLiveData<String>()
|
val result = MutableLiveData<String>()
|
||||||
@ -42,6 +43,7 @@ class ChatVM : BaseListViewModel<ChatItem>() {
|
|||||||
val itemId = itemIdCounter++
|
val itemId = itemIdCounter++
|
||||||
chatItems.add(ChatItem(itemId, question, ""))
|
chatItems.add(ChatItem(itemId, question, ""))
|
||||||
if (dataSourceReady) invalidate()
|
if (dataSourceReady) invalidate()
|
||||||
|
result.postValue(UUID.randomUUID().toString())
|
||||||
loading.postValue(true)
|
loading.postValue(true)
|
||||||
|
|
||||||
currentTask = HttpManager.getApi(Service::class.java).chat(ChatData(question))
|
currentTask = HttpManager.getApi(Service::class.java).chat(ChatData(question))
|
||||||
@ -63,7 +65,9 @@ class ChatVM : BaseListViewModel<ChatItem>() {
|
|||||||
loading.postValue(false)
|
loading.postValue(false)
|
||||||
return@use
|
return@use
|
||||||
}
|
}
|
||||||
val json = if (l.startsWith("data:")) l.removePrefix("data:").trim() else l
|
// 只处理 data:{...} 格式,其他行(event/id/注释等)跳过
|
||||||
|
if (!l.startsWith("data:{")) continue
|
||||||
|
val json = l.removePrefix("data:").trim()
|
||||||
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) {
|
||||||
loading.postValue(false)
|
loading.postValue(false)
|
||||||
|
|||||||
@ -21,6 +21,15 @@
|
|||||||
android:layout_width="40dp"
|
android:layout_width="40dp"
|
||||||
android:layout_height="40dp"
|
android:layout_height="40dp"
|
||||||
android:indeterminateDrawable="@drawable/load_progress" />
|
android:indeterminateDrawable="@drawable/load_progress" />
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pb1"
|
||||||
|
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="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:src="@drawable/loading"/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/hint1"
|
android:id="@+id/hint1"
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户