|
|
@@ -21,15 +21,50 @@ import retrofit2.HttpException
|
|
|
import java.util.UUID
|
|
|
|
|
|
class ChatVM : BaseListViewModel<ChatItem>() {
|
|
|
+
|
|
|
+ companion object {
|
|
|
+ const val SPACER_ID = -1
|
|
|
+ }
|
|
|
+
|
|
|
val result = MutableLiveData<String>()
|
|
|
val loading = MutableLiveData<Boolean>()
|
|
|
- val chatItems: MutableList<ChatItem> = mutableListOf()
|
|
|
+ /** 末尾始终保留一个占位 item(SPACER_ID),高度 = RecyclerView 高度,用于支持最新 item 滚到顶部 */
|
|
|
+ val chatItems: MutableList<ChatItem> = mutableListOf(ChatItem(SPACER_ID, "", ""))
|
|
|
|
|
|
private var currentTask: Disposable? = null
|
|
|
private var itemIdCounter = 0
|
|
|
private var dataSourceReady = false
|
|
|
private val mainHandler = Handler(Looper.getMainLooper())
|
|
|
|
|
|
+ /** 最后一个真实 item 的索引(占位 item 之前) */
|
|
|
+ private val lastRealIndex get() = chatItems.size - 2
|
|
|
+
|
|
|
+ // "思考中" 六点轮询动画
|
|
|
+ private var dotsRunnable: Runnable? = null
|
|
|
+ private var dotsCount = 0
|
|
|
+
|
|
|
+ private fun startThinkingAnimation() {
|
|
|
+ stopThinkingAnimation()
|
|
|
+ dotsCount = 1
|
|
|
+ dotsRunnable = object : Runnable {
|
|
|
+ override fun run() {
|
|
|
+ val idx = lastRealIndex
|
|
|
+ if (idx >= 0) {
|
|
|
+ chatItems[idx].content = "思考中" + "·".repeat(dotsCount)
|
|
|
+ notifyItem(idx)
|
|
|
+ }
|
|
|
+ dotsCount = dotsCount % 6 + 1
|
|
|
+ mainHandler.postDelayed(this, 300)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ mainHandler.post(dotsRunnable!!)
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun stopThinkingAnimation() {
|
|
|
+ dotsRunnable?.let { mainHandler.removeCallbacks(it) }
|
|
|
+ dotsRunnable = null
|
|
|
+ }
|
|
|
+
|
|
|
override fun loadData(page: Int, onResponse: Response<ChatItem>) {
|
|
|
dataSourceReady = true
|
|
|
if (page == 0) {
|
|
|
@@ -42,9 +77,11 @@ class ChatVM : BaseListViewModel<ChatItem>() {
|
|
|
fun demoPostSse(question: String) {
|
|
|
currentTask?.dispose()
|
|
|
currentTask = null
|
|
|
+ stopThinkingAnimation()
|
|
|
|
|
|
val itemId = itemIdCounter++
|
|
|
- chatItems.add(ChatItem(itemId, question, ""))
|
|
|
+ // 插入到占位 item 之前,保证占位 item 始终在末尾
|
|
|
+ chatItems.add(chatItems.size - 1, ChatItem(itemId, question, ""))
|
|
|
if (dataSourceReady) invalidate()
|
|
|
result.postValue(UUID.randomUUID().toString())
|
|
|
loading.postValue(true)
|
|
|
@@ -63,7 +100,8 @@ class ChatVM : BaseListViewModel<ChatItem>() {
|
|
|
if (l.trimStart().startsWith("<")) {
|
|
|
// HTML 响应(隧道断开),移除占位项并在主线程用相同问题重试
|
|
|
mainHandler.post {
|
|
|
- if (chatItems.isNotEmpty()) chatItems.removeAt(chatItems.size - 1)
|
|
|
+ val ri = lastRealIndex
|
|
|
+ if (ri >= 0) chatItems.removeAt(ri)
|
|
|
demoPostSse(question)
|
|
|
}
|
|
|
loading.postValue(false)
|
|
|
@@ -76,7 +114,7 @@ class ChatVM : BaseListViewModel<ChatItem>() {
|
|
|
if (model.type == null) {
|
|
|
loading.postValue(false)
|
|
|
val msg = model.msg ?: json
|
|
|
- val lastIndex = chatItems.size - 1
|
|
|
+ val lastIndex = lastRealIndex
|
|
|
if (lastIndex >= 0) {
|
|
|
chatItems[lastIndex].content = msg
|
|
|
notifyItem(lastIndex)
|
|
|
@@ -87,13 +125,18 @@ class ChatVM : BaseListViewModel<ChatItem>() {
|
|
|
if (model.role != "assistant") continue
|
|
|
when (model.type) {
|
|
|
"reason" -> {
|
|
|
- val m = GsonImplHelp.get().toObject(json, ChatModel2::class.java)
|
|
|
- content += m.data.content
|
|
|
- currentType = "reason"
|
|
|
+ if (currentType != "reason") {
|
|
|
+ // 第一个 reason 帧:启动六点轮询动画
|
|
|
+ currentType = "reason"
|
|
|
+ startThinkingAnimation()
|
|
|
+ }
|
|
|
+ // 内容由动画在主线程自行写入,此处跳过 notifyItem
|
|
|
+ continue
|
|
|
}
|
|
|
"string" -> {
|
|
|
if (currentType != "string") {
|
|
|
- // 第一条 string:清除 reason 阶段的内容,重新开始
|
|
|
+ // 第一条 string:停止动画,清空 reason 阶段内容
|
|
|
+ stopThinkingAnimation()
|
|
|
content = ""
|
|
|
currentType = "string"
|
|
|
}
|
|
|
@@ -102,7 +145,7 @@ class ChatVM : BaseListViewModel<ChatItem>() {
|
|
|
}
|
|
|
else -> continue
|
|
|
}
|
|
|
- val lastIndex = chatItems.size - 1
|
|
|
+ val lastIndex = lastRealIndex
|
|
|
if (lastIndex >= 0) {
|
|
|
chatItems[lastIndex].content = content
|
|
|
notifyItem(lastIndex)
|
|
|
@@ -112,9 +155,10 @@ class ChatVM : BaseListViewModel<ChatItem>() {
|
|
|
}
|
|
|
} catch (e: Exception) {
|
|
|
LogHelper.e(">>>>11", e)
|
|
|
+ stopThinkingAnimation()
|
|
|
loading.postValue(false)
|
|
|
val errMsg = "AI反馈异常: ${e.message}"
|
|
|
- val lastIndex = chatItems.size - 1
|
|
|
+ val lastIndex = lastRealIndex
|
|
|
if (lastIndex >= 0) {
|
|
|
chatItems[lastIndex].content = errMsg
|
|
|
notifyItem(lastIndex)
|
|
|
@@ -122,9 +166,11 @@ class ChatVM : BaseListViewModel<ChatItem>() {
|
|
|
result.postValue(errMsg)
|
|
|
}
|
|
|
}
|
|
|
+ stopThinkingAnimation()
|
|
|
loading.postValue(false)
|
|
|
}, { e ->
|
|
|
LogHelper.e(">>>>22", e)
|
|
|
+ stopThinkingAnimation()
|
|
|
loading.postValue(false)
|
|
|
val errMsg = if (e is HttpException) {
|
|
|
runCatching {
|
|
|
@@ -134,7 +180,7 @@ class ChatVM : BaseListViewModel<ChatItem>() {
|
|
|
} else {
|
|
|
"AI反馈异常: ${e.message}"
|
|
|
}
|
|
|
- val lastIndex = chatItems.size - 1
|
|
|
+ val lastIndex = lastRealIndex
|
|
|
if (lastIndex >= 0) {
|
|
|
chatItems[lastIndex].content = errMsg
|
|
|
notifyItem(lastIndex)
|