Skip to content

Commit 20382fc

Browse files
committed
feat: keep conversation streams alive across switches & scope chat state per-conversation
1 parent 69100b7 commit 20382fc

7 files changed

Lines changed: 113 additions & 20 deletions

File tree

ui/src/api/type/application.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,18 @@ export class ChatManagement {
548548
return chatRecord ? chatRecord.is_stop : false
549549
}
550550

551+
/**
552+
* 获取指定会话中仍在流式输出(尚未写完)的在途消息
553+
* 用于切回会话时, 把后台还在跑的流重新接回列表继续实时显示
554+
* @param chatId 会话id (chat.chat_id)
555+
* @returns 在途的 chat 对象列表
556+
*/
557+
static getActiveByChatId(chatId: string): chatType[] {
558+
return Object.values(this.chatMessageContainer)
559+
.filter((record) => record.chat.chat_id === chatId && !record.write_ed)
560+
.map((record) => record.chat)
561+
}
562+
551563
/**
552564
* 清除无用数据 也就是被close掉的和stop的数据
553565
*/

ui/src/components/ai-chat/component/answer-content/index.vue

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@
9999
</div>
100100
</template>
101101
<script setup lang="ts">
102-
import { computed, onMounted } from 'vue'
102+
import { computed } from 'vue'
103103
import KnowledgeSourceComponent from '@/components/ai-chat/component/knowledge-source-component/index.vue'
104104
import MdRenderer from '@/components/markdown/MdRenderer.vue'
105105
import OperationButton from '@/components/ai-chat/component/operation-button/index.vue'
@@ -210,11 +210,5 @@ const stopChat = (chat: chatType) => {
210210
const startChat = (chat: chatType) => {
211211
props.chatManagement.write(chat.id)
212212
}
213-
214-
onMounted(() => {
215-
bus.on('chat:stop', () => {
216-
stopChat(props.chatRecord)
217-
})
218-
})
219213
</script>
220214
<style lang="scss" scoped></style>

ui/src/components/ai-chat/component/chat-input-operate/index.vue

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -446,14 +446,8 @@ const chatId_context = computed({
446446
emit('update:chatId', v)
447447
},
448448
})
449-
const localLoading = computed({
450-
get: () => {
451-
return props.loading
452-
},
453-
set: (v) => {
454-
emit('update:loading', v)
455-
},
456-
})
449+
// 语音转写的请求 spinner, 独立于 loading prop(loading 现在是父级单向传入的"当前会话生成态")
450+
const speechLoading = ref(false)
457451
458452
const showURLSetting = ref(false)
459453
const urlForm = reactive({
@@ -807,7 +801,7 @@ const uploadRecording = async (audioBlob: Blob) => {
807801
if (props.applicationDetails.stt_autosend) {
808802
bus.emit('on:transcribing', true)
809803
}
810-
speechToTextAPI(props.applicationDetails.id as string, formData, localLoading)
804+
speechToTextAPI(props.applicationDetails.id as string, formData, speechLoading)
811805
.then((response) => {
812806
inputValue.value = typeof response.data === 'string' ? response.data : ''
813807
// 自动发送

ui/src/components/ai-chat/index.vue

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@
140140
<!-- 回答 -->
141141
<AnswerContent
142142
:application="applicationDetails"
143-
:loading="loading"
143+
:loading="currentChatGenerating"
144144
v-model:chat-record="chatList[index]"
145145
:type="type"
146146
:send-message="sendMessage"
@@ -202,7 +202,7 @@
202202
:validate="validate"
203203
:chat-management="ChatManagement"
204204
v-model:chat-id="chartOpenId"
205-
v-model:loading="loading"
205+
:loading="currentChatGenerating"
206206
v-model:show-user-input="showUserInput"
207207
v-else-if="type !== 'log' && type !== 'share'"
208208
>
@@ -310,6 +310,7 @@ const props = withDefaults(
310310
)
311311
const emit = defineEmits([
312312
'refresh',
313+
'openChat',
313314
'scroll',
314315
'openExecutionDetail',
315316
'openParagraph',
@@ -346,6 +347,12 @@ const loading = ref(false)
346347
const inputValue = ref<string>('')
347348
const chartOpenId = ref<string>('')
348349
const chatList = ref<any[]>([])
350+
// 当前正在查看的会话是否有在途消息(还在吐字)。
351+
// 用它驱动"停止回答"按钮、输入禁用、发送拦截, 替代组件级全局 loading,
352+
// 这样后台其它会话的流式不会把当前会话的输入栏按住。
353+
const currentChatGenerating = computed(() =>
354+
chatList.value.some((c) => c && c.write_ed === false && c.is_stop !== true),
355+
)
349356
const form_data = ref<any>({})
350357
const api_form_data = ref<any>({})
351358
const userFormRef = ref<InstanceType<typeof UserForm>>()
@@ -531,7 +538,7 @@ function sendMessage(val: string, other_params_data?: any, chat?: chatType): Pro
531538
532539
showUserInput.value = false
533540
534-
if (!loading.value && props.applicationDetails?.name) {
541+
if (!currentChatGenerating.value && props.applicationDetails?.name) {
535542
handleDebounceClick(val, other_params_data, chat)
536543
return true
537544
}
@@ -551,7 +558,7 @@ function sendMessage(val: string, other_params_data?: any, chat?: chatType): Pro
551558
}
552559
} else {
553560
showUserInput.value = false
554-
if (!loading.value && props.applicationDetails?.name) {
561+
if (!currentChatGenerating.value && props.applicationDetails?.name) {
555562
handleDebounceClick(val, other_params_data, chat)
556563
return Promise.resolve(true)
557564
}
@@ -656,6 +663,16 @@ const errorWrite = (chat: any, message?: string) => {
656663
ChatManagement.close(chat.id)
657664
}
658665
666+
// 停止"当前正在查看的会话"里在途的消息。
667+
// 只动 chatList(当前会话), 不会波及后台其它正在跑的会话。
668+
const stopGenerating = () => {
669+
chatList.value.forEach((c) => {
670+
if (c && c.write_ed === false && c.is_stop !== true) {
671+
ChatManagement.stop(c.id)
672+
}
673+
})
674+
}
675+
659676
// 保存上传文件列表
660677
661678
function chatMessage(chat?: any, problem?: string, re_chat?: boolean, other_params_data?: any) {
@@ -731,6 +748,11 @@ function chatMessage(chat?: any, problem?: string, re_chat?: boolean, other_para
731748
} else if (response.status === 461) {
732749
return Promise.reject(t('aiChat.tip.errorLimitMessage'))
733750
} else {
751+
// 新建会话: 此刻后端已执行 set_chat 建好 Chat 行(在产出流之前),
752+
// 通知父级把新会话加入历史列表, 这样长回答流式期间切走也能切回来继续看
753+
if (props.chatId === 'new') {
754+
emit('openChat', chartOpenId.value)
755+
}
734756
nextTick(() => {
735757
// 将滚动条滚动到最下面
736758
scrollDiv.value.setScrollTop(getMaxHeight())
@@ -907,11 +929,13 @@ onMounted(() => {
907929
checkAll.value = multipleSelectionChat.value.length === chatList.value.length
908930
emit('update:selection', true)
909931
})
932+
bus.on('chat:stop', stopGenerating)
910933
})
911934
912935
onBeforeUnmount(() => {
913936
window.sendMessage = null
914937
window.chatUserProfile = null
938+
bus.off('chat:stop', stopGenerating)
915939
})
916940
917941
function setScrollBottom() {

ui/src/views/chat/embed/index.vue

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
:chatId="currentChatId"
7878
type="ai-chat"
7979
@refresh="refresh"
80+
@openChat="refresh"
8081
@scroll="handleScroll"
8182
class="AiChat-embed"
8283
v-model:selection="showSelection"
@@ -107,6 +108,7 @@ import { hexToRgba } from '@/utils/theme'
107108
import { t } from '@/locales'
108109
import ChatHistoryDrawer from './component/ChatHistoryDrawer.vue'
109110
import chatAPI from '@/api/chat/chat'
111+
import { ChatManagement } from '@/api/type/application'
110112
111113
provide('scrollData', loadInfiniteScroll)
112114
provide('chatLogPagination', () => chatLogPagination)
@@ -223,6 +225,26 @@ function loadInfiniteScroll() {
223225
getChatLog(true)
224226
}
225227
228+
/**
229+
* 切回会话时, 把内存中属于该会话、仍在后台流式输出的在途消息接回列表,
230+
* 这样切走时没被打断的流, 切回来能继续实时显示。
231+
* - 与 DB 记录 record_id 相同的, 用 live 对象覆盖(否则会显示落库前的空答案)
232+
* - DB 里还没有的(尚未落库), 追加到末尾
233+
*/
234+
function attachActiveStreams() {
235+
const activeChats = ChatManagement.getActiveByChatId(currentChatId.value)
236+
if (!activeChats.length) {
237+
return
238+
}
239+
const activeMap = new Map(activeChats.map((chat) => [chat.record_id, chat]))
240+
const existIds = new Set(currentRecordList.value.map((v: any) => v.record_id))
241+
const merged = currentRecordList.value.map((v: any) =>
242+
activeMap.has(v.record_id) ? activeMap.get(v.record_id) : v,
243+
)
244+
const appendList = activeChats.filter((chat) => !existIds.has(chat.record_id))
245+
currentRecordList.value = [...merged, ...appendList]
246+
}
247+
226248
function getChatRecord() {
227249
return chatAPI
228250
.pageChatRecord(
@@ -242,6 +264,7 @@ function getChatRecord() {
242264
a.create_time.localeCompare(b.create_time),
243265
)
244266
if (paginationConfig.current_page === 1) {
267+
attachActiveStreams()
245268
nextTick(() => {
246269
// 将滚动条滚动到最下面
247270
AiChatRef.value.setScrollBottom()

ui/src/views/chat/mobile/index.vue

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
:chatId="currentChatId"
7878
type="ai-chat"
7979
@refresh="refresh"
80+
@openChat="refresh"
8081
@scroll="handleScroll"
8182
v-model:selection="showSelection"
8283
>
@@ -106,6 +107,7 @@ import useStore from '@/stores'
106107
import { t } from '@/locales'
107108
import ChatHistoryDrawer from './component/ChatHistoryDrawer.vue'
108109
import chatAPI from '@/api/chat/chat'
110+
import { ChatManagement } from '@/api/type/application'
109111
110112
provide('scrollData', loadInfiniteScroll)
111113
provide('chatLogPagination', () => chatLogPagination)
@@ -227,6 +229,26 @@ function loadInfiniteScroll() {
227229
getChatLog(true)
228230
}
229231
232+
/**
233+
* 切回会话时, 把内存中属于该会话、仍在后台流式输出的在途消息接回列表,
234+
* 这样切走时没被打断的流, 切回来能继续实时显示。
235+
* - 与 DB 记录 record_id 相同的, 用 live 对象覆盖(否则会显示落库前的空答案)
236+
* - DB 里还没有的(尚未落库), 追加到末尾
237+
*/
238+
function attachActiveStreams() {
239+
const activeChats = ChatManagement.getActiveByChatId(currentChatId.value)
240+
if (!activeChats.length) {
241+
return
242+
}
243+
const activeMap = new Map(activeChats.map((chat) => [chat.record_id, chat]))
244+
const existIds = new Set(currentRecordList.value.map((v: any) => v.record_id))
245+
const merged = currentRecordList.value.map((v: any) =>
246+
activeMap.has(v.record_id) ? activeMap.get(v.record_id) : v,
247+
)
248+
const appendList = activeChats.filter((chat) => !existIds.has(chat.record_id))
249+
currentRecordList.value = [...merged, ...appendList]
250+
}
251+
230252
function getChatRecord() {
231253
return chatAPI
232254
.pageChatRecord(
@@ -246,6 +268,7 @@ function getChatRecord() {
246268
a.create_time.localeCompare(b.create_time),
247269
)
248270
if (paginationConfig.current_page === 1) {
271+
attachActiveStreams()
249272
nextTick(() => {
250273
// 将滚动条滚动到最下面
251274
AiChatRef.value.setScrollBottom()

ui/src/views/chat/pc/index.vue

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@
172172
:chatId="currentChatId"
173173
executionIsRightPanel
174174
@refresh="refresh"
175+
@openChat="refresh"
175176
@scroll="handleScroll"
176177
@open-execution-detail="openExecutionDetail"
177178
@openParagraph="openKnowledgeSource"
@@ -258,6 +259,7 @@ import ExecutionDetailContent from '@/components/ai-chat/component/knowledge-sou
258259
import ParagraphSourceContent from '@/components/ai-chat/component/knowledge-source-component/ParagraphSourceContent.vue'
259260
import ParagraphDocumentContent from '@/components/ai-chat/component/knowledge-source-component/ParagraphDocumentContent.vue'
260261
import HistoryPanel from '@/views/chat/component/HistoryPanel.vue'
262+
import { ChatManagement } from '@/api/type/application'
261263
import { cloneDeep } from 'lodash'
262264
import { getFileUrl } from '@/utils/common'
263265
import PdfExport from '@/components/pdf-export/index.vue'
@@ -443,6 +445,26 @@ function loadInfiniteScroll() {
443445
getChatLog(true)
444446
}
445447
448+
/**
449+
* 切回会话时, 把内存中属于该会话、仍在后台流式输出的在途消息接回列表,
450+
* 这样切走时没被打断的流, 切回来能继续实时显示。
451+
* - 与 DB 记录 record_id 相同的, 用 live 对象覆盖(否则会显示落库前的空答案)
452+
* - DB 里还没有的(尚未落库), 追加到末尾
453+
*/
454+
function attachActiveStreams() {
455+
const activeChats = ChatManagement.getActiveByChatId(currentChatId.value)
456+
if (!activeChats.length) {
457+
return
458+
}
459+
const activeMap = new Map(activeChats.map((chat) => [chat.record_id, chat]))
460+
const existIds = new Set(currentRecordList.value.map((v: any) => v.record_id))
461+
const merged = currentRecordList.value.map((v: any) =>
462+
activeMap.has(v.record_id) ? activeMap.get(v.record_id) : v,
463+
)
464+
const appendList = activeChats.filter((chat) => !existIds.has(chat.record_id))
465+
currentRecordList.value = [...merged, ...appendList]
466+
}
467+
446468
function getChatRecord() {
447469
return chatAPI
448470
.pageChatRecord(
@@ -462,6 +484,7 @@ function getChatRecord() {
462484
a.create_time.localeCompare(b.create_time),
463485
)
464486
if (paginationConfig.value.current_page === 1) {
487+
attachActiveStreams()
465488
nextTick(() => {
466489
// 将滚动条滚动到最下面
467490
AiChatRef.value.setScrollBottom()

0 commit comments

Comments
 (0)