diff --git a/easyflow-ui-admin/app/src/components/chat-history/ChatHistoryDetailDrawer.vue b/easyflow-ui-admin/app/src/components/chat-history/ChatHistoryDetailDrawer.vue index bfe94e4..b2d6863 100644 --- a/easyflow-ui-admin/app/src/components/chat-history/ChatHistoryDetailDrawer.vue +++ b/easyflow-ui-admin/app/src/components/chat-history/ChatHistoryDetailDrawer.vue @@ -1,26 +1,16 @@ + + + + diff --git a/easyflow-ui-admin/app/src/components/chat/chat.vue b/easyflow-ui-admin/app/src/components/chat/chat.vue index 19017c4..0bad282 100644 --- a/easyflow-ui-admin/app/src/components/chat/chat.vue +++ b/easyflow-ui-admin/app/src/components/chat/chat.vue @@ -5,8 +5,7 @@ import type { } from 'vue-element-plus-x/types/BubbleList'; import type { TypewriterInstance } from 'vue-element-plus-x/types/Typewriter'; -import type { ChatThinkingBlockStatus } from '@easyflow/common-ui'; -import type { BotInfo, ChatMessage } from '@easyflow/types'; +import type { BotInfo, ChatTimeTimelineItem } from '@easyflow/types'; import { nextTick, @@ -18,59 +17,36 @@ import { } from 'vue'; import ElBubbleList from 'vue-element-plus-x/es/BubbleList/index.js'; import ElSender from 'vue-element-plus-x/es/Sender/index.js'; -import ElXMarkdown from 'vue-element-plus-x/es/XMarkdown/index.js'; import { useRoute, useRouter } from 'vue-router'; -import { ChatThinkingBlock } from '@easyflow/common-ui'; -import { IconifyIcon } from '@easyflow/icons'; import { $t } from '@easyflow/locales'; import { useBotStore } from '@easyflow/stores'; -import { cloneDeep, cn, uuid } from '@easyflow/utils'; +import { + ChatTimeHistoryMapper, + ChatTimeTimelineBuilder, + cn, + uuid, +} from '@easyflow/utils'; import { ArrowDownBold, - CircleCheck, CopyDocument, Paperclip, RefreshRight, } from '@element-plus/icons-vue'; -import { - ElButton, - ElCollapse, - ElCollapseItem, - ElIcon, - ElMessage, - ElSpace, -} from 'element-plus'; +import { ElButton, ElIcon, ElMessage, ElSpace } from 'element-plus'; import { tryit } from 'radash'; import { getMessageList, getPerQuestions } from '#/api'; import { api, sseClient } from '#/api/request'; +import ChatTimeMessageContent from '#/components/chat/ChatTimeMessageContent.vue'; import SendEnableIcon from '#/components/icons/SendEnableIcon.vue'; import SendIcon from '#/components/icons/SendIcon.vue'; -import ShowJson from '#/components/json/ShowJson.vue'; import ChatFileUploader from '#/components/upload/ChatFileUploader.vue'; import BotAvatar from '../botAvatar/botAvatar.vue'; import SendingIcon from '../icons/SendingIcon.vue'; -type Think = { - reasoning_content?: string; - thinkingExpanded?: boolean; - thinkingStatus?: ChatThinkingBlockStatus; -}; - -type Tool = { - id: string; - name: string; - result: string; - status: 'TOOL_CALL' | 'TOOL_RESULT'; -}; - -type MessageItem = ChatMessage & { - chains?: (Think | Tool)[]; -}; - const props = defineProps<{ bot?: BotInfo; conversationId?: string; @@ -90,7 +66,7 @@ const route = useRoute(); const botId = ref((route.params.id as string) || ''); const router = useRouter(); -const bubbleItems = ref['list']>([]); +const bubbleItems = ref['list']>([]); const bubbleListRef = ref(); const messageContainerRef = ref(null); const bubbleListScrollElement = ref(null); @@ -153,14 +129,9 @@ watchEffect(async () => { }); if (res?.errorCode === 0) { - bubbleItems.value = res.data.map((item) => ({ - ...item, - content: - item.role === 'assistant' - ? item.content.replace(/^Final Answer:\s*/i, '') - : item.content, - placement: item.role === 'assistant' ? 'start' : 'end', - })); + bubbleItems.value = ChatTimeHistoryMapper.fromHistoryRecords( + res.data as any[], + ); } } else { bubbleItems.value = []; @@ -207,31 +178,17 @@ const bindBubbleListScroll = () => { } updateBackToBottomButtonVisible(); }; -const updateLastBubbleItem = (patch: Partial) => { - const lastIndex = bubbleItems.value.length - 1; - if (lastIndex < 0) { - return; - } - bubbleItems.value[lastIndex] = { - ...bubbleItems.value[lastIndex]!, - ...patch, - }; -}; -const finalizeLastBubbleItem = () => { - updateLastBubbleItem({ - loading: false, - typing: false, - }); +const finalizeTimelineTail = () => { + ChatTimeTimelineBuilder.finalize(bubbleItems.value); }; const stopSse = () => { sseClient.abort(); sending.value = false; - finalizeLastBubbleItem(); + finalizeTimelineTail(); }; const clearSenderFiles = () => { files.value = []; attachmentsRef.value?.clearFiles(); - openCloseHeader(); }; const handleSubmit = async (refreshContent: string) => { const attachments = attachmentsRef.value?.getFileList(); @@ -261,14 +218,11 @@ const handleSubmit = async (refreshContent: string) => { sseClient.post('/api/v1/bot/chat', data, { onMessage(message) { const event = message.event; - const lastIndex = bubbleItems.value.length - 1; - const lastBubbleItem = bubbleItems.value[lastIndex]; // finish if (event === 'done') { sending.value = false; - finalizeLastBubbleItem(); - stopThinking(); + finalizeTimelineTail(); return; } if (!message.data) { @@ -280,46 +234,30 @@ const handleSubmit = async (refreshContent: string) => { sseData?.domain === 'SYSTEM' && sseData.payload?.code === 'SYSTEM_ERROR' ) { - const errorMessage = sseData.payload.message; - if (!lastBubbleItem) return; - bubbleItems.value[lastIndex] = { - ...lastBubbleItem, - content: errorMessage, - loading: false, - typing: false, - }; + ChatTimeTimelineBuilder.applySystemError( + bubbleItems.value, + sseData.payload.message, + Date.now(), + ); return; } - if (lastIndex >= 0 && sseData?.domain === 'TOOL') { - const chains = cloneDeep(lastBubbleItem?.chains ?? []); - const index = chains.findIndex( - (chain) => - isTool(chain) && chain.id === sseData?.payload?.tool_call_id, - ); - - if (index === -1) { - chains.push({ - id: sseData?.payload?.tool_call_id, + if (sseData?.domain === 'TOOL') { + if (sseData?.type === 'TOOL_CALL') { + ChatTimeTimelineBuilder.upsertToolCall(bubbleItems.value, { + created: Date.now(), name: sseData?.payload?.name, - status: sseData?.type, - result: - sseData?.type === 'TOOL_CALL' - ? sseData?.payload?.arguments - : sseData?.payload?.result, + toolCallId: sseData?.payload?.tool_call_id, + value: sseData?.payload?.arguments, }); } else { - chains[index] = { - ...chains[index]!, - status: sseData?.type, - result: - sseData?.type === 'TOOL_CALL' - ? sseData?.payload?.arguments - : sseData?.payload?.result, - }; + ChatTimeTimelineBuilder.upsertToolResult(bubbleItems.value, { + created: Date.now(), + name: sseData?.payload?.name, + result: sseData?.payload?.result, + toolCallId: sseData?.payload?.tool_call_id, + }); } - bubbleItems.value[lastIndex]!.chains = chains; - stopThinking(); return; } @@ -327,38 +265,19 @@ const handleSubmit = async (refreshContent: string) => { const delta = sseData.payload?.delta; const role = sseData.payload?.role; - if (lastBubbleItem && delta) { + if (delta) { if (sseData.type === 'THINKING') { - const chains = cloneDeep(lastBubbleItem?.chains ?? []); - const index = chains.findIndex( - (chain) => isThink(chain) && chain.thinkingStatus === 'thinking', + ChatTimeTimelineBuilder.appendThinkingDelta( + bubbleItems.value, + delta, + Date.now(), ); - - if (index === -1) { - chains.push({ - thinkingStatus: 'thinking', - thinkingExpanded: false, - reasoning_content: delta, - }); - } else { - const think = chains[index]! as Think; - chains[index] = { - ...think, - reasoning_content: think.reasoning_content + delta, - }; - } - bubbleItems.value[lastIndex]!.chains = chains; } else if (sseData.type === 'MESSAGE') { - bubbleItems.value[lastIndex] = { - ...lastBubbleItem, - content: (lastBubbleItem.content + delta).replaceAll( - '```echartsoption', - '```echarts\noption', - ), - loading: false, - typing: true, - }; - stopThinking(); + ChatTimeTimelineBuilder.appendMessageDelta( + bubbleItems.value, + delta, + Date.now(), + ); } } @@ -372,39 +291,16 @@ const handleSubmit = async (refreshContent: string) => { }, onFinished() { sending.value = false; - finalizeLastBubbleItem(); - stopThinking(); + finalizeTimelineTail(); }, onError(err) { console.error(err); sending.value = false; - finalizeLastBubbleItem(); + finalizeTimelineTail(); }, }); }; -const isTool = (item: Think | Tool) => { - return 'id' in item; -}; -const isThink = (item: Think | Tool): item is Think => { - return !('id' in item); -}; -const stopThinking = () => { - const lastIndex = bubbleItems.value.length - 1; - - if (lastIndex >= 0 && bubbleItems.value[lastIndex]?.chains) { - const chains = cloneDeep(bubbleItems.value[lastIndex].chains); - - for (const chain of chains) { - if (isThink(chain) && chain.thinkingStatus === 'thinking') { - chain.thinkingStatus = 'end'; - } - } - - bubbleItems.value[lastIndex].chains = chains; - } -}; - const handleComplete = (_: TypewriterInstance, index: number) => { if ( index === bubbleItems.value.length - 1 && @@ -421,27 +317,14 @@ const handleComplete = (_: TypewriterInstance, index: number) => { }; const generateMockMessages = (refreshContent: string) => { - const userMessage: MessageItem = { + const userMessage: ChatTimeTimelineItem = { role: 'user', id: Date.now().toString(), - fileList: [], content: refreshContent || senderValue.value, created: Date.now(), - updateAt: Date.now(), placement: 'end', }; - - const assistantMessage: MessageItem = { - role: 'assistant', - id: Date.now().toString(), - content: '', - loading: true, - created: Date.now(), - updateAt: Date.now(), - placement: 'start', - }; - - return [userMessage, assistantMessage]; + return [userMessage]; }; const handleCopy = (content: string) => { @@ -462,23 +345,17 @@ const scrollToBottom = () => { } showBackToBottomButton.value = false; }; -const showHeaderFlog = ref(false); -function openCloseHeader() { - if (showHeaderFlog.value) { - senderRef.value?.closeHeader(); - files.value = []; - } else { - senderRef.value?.openHeader(); - } - showHeaderFlog.value = !showHeaderFlog.value; -} const attachmentsRef = ref(); const files = ref([]); function handlePasteFile(_: any, fileList: FileList) { - showHeaderFlog.value = true; - senderRef.value?.openHeader(); files.value = [...fileList]; } +function triggerFileSelect() { + attachmentsRef.value?.triggerFileSelect?.(); +} +function handleDeleteAllSenderFiles() { + files.value = []; +} watch( () => [localeConversationId.value, bubbleItems.value.length], () => { @@ -527,78 +404,30 @@ onBeforeUnmount(() => { {{ new Date(item.created).toLocaleString() }} - -