diff --git a/ui/src/api/admin/workspace/application/chat-log.ts b/ui/src/api/admin/workspace/application/chat-log.ts index b877a8a49a9..2dfaf6371d3 100644 --- a/ui/src/api/admin/workspace/application/chat-log.ts +++ b/ui/src/api/admin/workspace/application/chat-log.ts @@ -1,6 +1,6 @@ import { del, get, post, postExportExcel, put } from '../../core/request' import type { ParamsPage, ResponsePage } from '../../core/types' -import type { ChatLog, ChatLogAddKnowledgePayload, Dict } from '@/api/types' +import type { ChatLog, ChatLogAddKnowledgePayload, ChatLogRecord, Dict } from '@/api/types' import { getWorkspaceId } from '@/utils/resource-context' const getPrefix = () => `/workspace/${getWorkspaceId()}/application` @@ -27,7 +27,7 @@ const postChatLogAddKnowledge = (applicationId: string, data: ChatLogAddKnowledg /** 分页获取对话中的聊天记录。 */ const getChatRecordLog = (applicationId: string, chatId: string, page: ParamsPage, orderAsc = true) => - get>>(`${getPrefix()}/${applicationId}/chat/${chatId}/chat_record/${page.currentPage}/${page.pageSize}`, { + get>(`${getPrefix()}/${applicationId}/chat/${chatId}/chat_record/${page.currentPage}/${page.pageSize}`, { order_asc: orderAsc, }) diff --git a/ui/src/api/types/chat-log.ts b/ui/src/api/types/chat-log.ts index 2e5585fb9cc..7f2cd3ab77b 100644 --- a/ui/src/api/types/chat-log.ts +++ b/ui/src/api/types/chat-log.ts @@ -1,4 +1,5 @@ import type { CHAT_LOG_SOURCE } from '@/api/enums' +import type { Dict } from './common' export type ChatLogSource = (typeof CHAT_LOG_SOURCE)[keyof typeof CHAT_LOG_SOURCE] @@ -22,3 +23,17 @@ export interface ChatLog { source?: { type?: string } | null update_time: string } + +/** 对话日志中的单条问答记录,仅列出前端使用的字段。 */ +export interface ChatLogRecord { + id: string + /** 提问内容项 */ + question: Dict + /** 回答内容项列表 */ + messages: Dict[] + message_tokens?: number + answer_tokens?: number + run_time?: number + paragraph_list?: Dict[] + execution_details?: Dict[] +} diff --git a/ui/src/conversation-panel/components/log-message-list/index.ts b/ui/src/conversation-panel/components/log-message-list/index.ts new file mode 100644 index 00000000000..65e6dabee3e --- /dev/null +++ b/ui/src/conversation-panel/components/log-message-list/index.ts @@ -0,0 +1,84 @@ +import { ref, inject, type InjectionKey } from 'vue' +import type { ChatLogRecord } from '@/api/types' +import type { ChatMessage, ChatRecordMeta } from '../../core/types' + +// 只读会话记录(如对话日志):只分页拉取历史记录,不发送、不续跑、不流式。 +// 与 message-list 刻意不共用 store 约定,也不注入 MESSAGE_LIST_KEY,表单等内容项因此自动只读。 +export interface LogMessageListDeps { + pageRecords: (chatId: string, page: number, size: number) => Promise<{ records: ChatLogRecord[] }> + getChatId: () => string +} + +// 从后端 chat_record(reset_chat_record 结构)提取底部信息栏数据 +const buildRecordMeta = (raw: ChatLogRecord): ChatRecordMeta => ({ + messageTokens: raw.message_tokens || 0, + answerTokens: raw.answer_tokens || 0, + runTime: raw.run_time || 0, + knowledgeCount: (raw.paragraph_list || []).length, + executionDetails: raw.execution_details || [], +}) + +export function createLogMessageListStore(deps: LogMessageListDeps) { + const { pageRecords, getChatId } = deps + + const messages = ref([]) + const loading = ref(false) + + const pageSize = 20 + const currentPage = ref(1) + const hasMore = ref(true) + + // 接口按最新在前分页:页内 reverse 成正序,更旧的页拼到前面 + const loadMessages = async (cid: string, page = 1) => { + loading.value = true + try { + const res = await pageRecords(cid, page, pageSize) + const records = [...(res?.records || [])].reverse() + const result = records.flatMap((record): ChatMessage[] => [ + { role: 'USER', content: [record.question], id: record.id + '_USER' }, + { + role: 'ASSISTANT', + content: record.messages, + id: record.id + '_ASSISTANT', + recordId: record.id, + record: buildRecordMeta(record), + }, + ]) + messages.value = page === 1 ? result : [...result, ...messages.value] + currentPage.value = page + hasMore.value = records.length >= pageSize + } catch { + // 静默处理 + } finally { + loading.value = false + } + } + + // 切换记录时由使用方主动调用。先清空再拉:无消息时的 loading 即首页加载 + const load = async () => { + const cid = getChatId() + messages.value = [] + currentPage.value = 1 + hasMore.value = true + if (!cid) return + await loadMessages(cid, 1) + } + + const loadMore = async () => { + const cid = getChatId() + if (!hasMore.value || loading.value || !cid) return + await loadMessages(cid, currentPage.value + 1) + } + + return { messages, loading, hasMore, load, loadMore } +} + +export type LogMessageListStore = ReturnType + +export const LOG_MESSAGE_LIST_KEY: InjectionKey = Symbol('log-message-list') + +export function useLogMessageListStore(): LogMessageListStore { + const store = inject(LOG_MESSAGE_LIST_KEY) + if (!store) throw new Error('useLogMessageListStore 必须在提供 LOG_MESSAGE_LIST_KEY 的视图内使用') + return store +} diff --git a/ui/src/conversation-panel/components/log-message-list/index.vue b/ui/src/conversation-panel/components/log-message-list/index.vue new file mode 100644 index 00000000000..5e571a17ed2 --- /dev/null +++ b/ui/src/conversation-panel/components/log-message-list/index.vue @@ -0,0 +1,97 @@ + + + + + diff --git a/ui/src/conversation-panel/components/message-input/index.ts b/ui/src/conversation-panel/components/message-input/index.ts index 4de83ab5a7f..1d7a0986896 100644 --- a/ui/src/conversation-panel/components/message-input/index.ts +++ b/ui/src/conversation-panel/components/message-input/index.ts @@ -32,7 +32,8 @@ export interface MessageInputDeps { composerResetSignal: Ref // 来自 message-list messages: Ref - loading: Ref + /** 对话加载(回答生成中):只读,由 message-list 维护 */ + streamLoading: Readonly> sendMessage: (opts: SendMessageOptions) => void uploadFile: (file: File, chatId: string, onProgress?: (percent: number) => void) => Promise stop: () => void @@ -47,7 +48,7 @@ const UPLOAD_PROGRESS_PENDING_MAX = 95 * message-input 组件 store:本组件独有——输入内容 + 暂存文件 + 发送。 */ export function createMessageInputStore(deps: MessageInputDeps) { - const { conversations, getChatId, renameChat, composerResetSignal, messages, loading, sendMessage, uploadFile, stop } = deps + const { conversations, getChatId, renameChat, composerResetSignal, messages, streamLoading, sendMessage, uploadFile, stop } = deps const disabled = computed(() => deps.disabled?.value ?? false) // 输入状态与附件限制 @@ -62,7 +63,7 @@ export function createMessageInputStore(deps: MessageInputDeps) { const placeholder = computed(() => { if (disabled.value) return '暂无可用智能体' - return loading.value ? '正在回复中...' : '输入消息...' + return streamLoading.value ? '正在回复中...' : '输入消息...' }) // 附件上传完成前不能发送,否则消息中的附件没有地址 const isUploading = computed(() => fileList.value.some((attachment) => attachment.uploading)) @@ -70,7 +71,7 @@ export function createMessageInputStore(deps: MessageInputDeps) { () => !disabled.value && (question.value.trim().length > 0 || fileList.value.length > 0) && - !loading.value && + !streamLoading.value && !sending.value && !isUploading.value, ) @@ -179,7 +180,6 @@ export function createMessageInputStore(deps: MessageInputDeps) { clearFiles() } catch (e) { console.error('send failed:', e) - loading.value = false } finally { sending.value = false } @@ -200,7 +200,7 @@ export function createMessageInputStore(deps: MessageInputDeps) { maxFiles, maxSizeMB, acceptList, - loading, + streamLoading, disabled, placeholder, canSend, diff --git a/ui/src/conversation-panel/components/message-input/index.vue b/ui/src/conversation-panel/components/message-input/index.vue index 12ac2e11bb6..b3d2b0aba26 100644 --- a/ui/src/conversation-panel/components/message-input/index.vue +++ b/ui/src/conversation-panel/components/message-input/index.vue @@ -5,7 +5,7 @@ import { formatFileSize } from '@/utils/number' import { inputShortcut } from '../../core/shortcuts' import { useMessageInputStore } from './index' -const { question, fileList, maxFiles, maxSizeMB, acceptList, loading, disabled, placeholder, canSend, imageFiles, addFiles, removeFile, send, stop } = +const { question, fileList, maxFiles, maxSizeMB, acceptList, streamLoading, disabled, placeholder, canSend, imageFiles, addFiles, removeFile, send, stop } = useMessageInputStore() // 选择和粘贴附件统一交给输入 Store 处理。 @@ -117,13 +117,13 @@ const handleKeydown = (event: KeyboardEvent) => { - + - + diff --git a/ui/src/conversation-panel/components/message-list/index.ts b/ui/src/conversation-panel/components/message-list/index.ts index 9603794224d..da3f22f645a 100644 --- a/ui/src/conversation-panel/components/message-list/index.ts +++ b/ui/src/conversation-panel/components/message-list/index.ts @@ -30,6 +30,7 @@ export function createMessageListStore(deps: MessageListDeps) { const messages = ref([]) const loading = ref(false) + const streamLoading = ref(false) // ── 分页 ───────────────────────────────────────────── const pageSize = 20 @@ -103,8 +104,10 @@ export function createMessageListStore(deps: MessageListDeps) { let currentStream: ConversationStream | null = null const closeStream = () => { if (currentStream) { + // 主动取消的流不会回调 onComplete,需在此复位对话加载 currentStream.cancel() currentStream = null + streamLoading.value = false return true } return false @@ -131,6 +134,7 @@ export function createMessageListStore(deps: MessageListDeps) { onComplete: (e?: any) => void, ) => { closeStream() + streamLoading.value = true request() .then((response: any) => { currentStream = new ConversationStream( @@ -141,12 +145,16 @@ export function createMessageListStore(deps: MessageListDeps) { }, (e) => { currentStream = null + streamLoading.value = false onComplete(e) }, ) currentStream.start() }) - .catch((e) => onComplete(e ?? new Error('stream request failed'))) + .catch((e) => { + streamLoading.value = false + onComplete(e ?? new Error('stream request failed')) + }) } // ── 发送 / 续跑 ───────────────────────────────────── @@ -173,7 +181,6 @@ export function createMessageListStore(deps: MessageListDeps) { if (opts.chatRecordId) payload.chat_record_id = opts.chatRecordId if (opts.chunkId) payload.chunk_id = opts.chunkId - loading.value = true runStream( () => chatMessage(cid, payload), (chunk) => { @@ -183,7 +190,6 @@ export function createMessageListStore(deps: MessageListDeps) { }, (e) => { target.write_ed = true - loading.value = false if (!e) getChatRecord(target, cid) opts.onComplete?.(e) }, @@ -203,17 +209,15 @@ export function createMessageListStore(deps: MessageListDeps) { } // ── 切换会话:由 conversation-list 主动调用(list 先写好 currentChatId 再调本函数)── + // 先清空再拉:无消息时的 loading 即首页加载(整块遮罩),有消息时的 loading 即加载更多 const load = async () => { const cid = getChatId() closeStream() - if (!cid) { - resetMessages() - return - } + resetMessages() + if (!cid) return await loadMessages(cid, 1) const lastMsg = messages.value[messages.value.length - 1] if (!lastMsg || (lastMsg.content && lastMsg.content.length > 0)) return - loading.value = true const recordId = lastMsg.id.replace('_USER', '').replace('_ASSISTANT', '') runStream( () => resumeMessage(cid, recordId), @@ -230,7 +234,6 @@ export function createMessageListStore(deps: MessageListDeps) { getChatRecord(m, cid) } } - loading.value = false }, ) } @@ -238,6 +241,7 @@ export function createMessageListStore(deps: MessageListDeps) { return { messages, loading, + streamLoading, hasMore, load, loadMore, diff --git a/ui/src/conversation-panel/components/message-list/index.vue b/ui/src/conversation-panel/components/message-list/index.vue index b25583c30c4..7253a262c38 100644 --- a/ui/src/conversation-panel/components/message-list/index.vue +++ b/ui/src/conversation-panel/components/message-list/index.vue @@ -1,13 +1,11 @@