From 00a8410c94348baef77190156d8be5b7af9721fb Mon Sep 17 00:00:00 2001 From: fdc <2213070223@qq.com> Date: Thu, 31 Jul 2025 09:51:25 +0800 Subject: [PATCH] feat:webchat frontend stream --- .../components/debug-dialog/DebugDialog.tsx | 112 +++++++++++++++--- web/src/app/infra/http/HttpClient.ts | 83 +++++++++++++ web/src/i18n/locales/en-US.ts | 1 + web/src/i18n/locales/ja-JP.ts | 1 + web/src/i18n/locales/zh-Hans.ts | 1 + 5 files changed, 181 insertions(+), 17 deletions(-) diff --git a/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx b/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx index a84389e0..9fde4bc2 100644 --- a/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx +++ b/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx @@ -10,6 +10,7 @@ import { cn } from '@/lib/utils'; import { Message } from '@/app/infra/entities/message'; import { toast } from 'sonner'; import AtBadge from './AtBadge'; +import { Switch } from '@/components/ui/switch'; interface MessageComponent { type: 'At' | 'Plain'; @@ -36,6 +37,7 @@ export default function DebugDialog({ const [showAtPopover, setShowAtPopover] = useState(false); const [hasAt, setHasAt] = useState(false); const [isHovering, setIsHovering] = useState(false); + const [isStreaming, setIsStreaming] = useState(false); const messagesEndRef = useRef(null); const inputRef = useRef(null); const popoverRef = useRef(null); @@ -157,27 +159,96 @@ export default function DebugDialog({ // for showing text_content = '@webchatbot' + text_content; } - const userMessage: Message = { - id: -1, - role: 'user', - content: text_content, - timestamp: new Date().toISOString(), - message_chain: messageChain, - }; + id: -1, + role: 'user', + content: text_content, + timestamp: new Date().toISOString(), + message_chain: messageChain, + }; + // 根据isStreaming状态决定使用哪种传输方式 + if (isStreaming) { + // 创建初始bot消息 + const botMessage: Message = { + id: -1, + role: 'assistant', + content: '', + timestamp: new Date().toISOString(), + message_chain: [{ type: 'Plain', text: '' }], + }; - setMessages((prevMessages) => [...prevMessages, userMessage]); - setInputValue(''); - setHasAt(false); + // 添加用户消息和初始bot消息到状态 - const response = await httpClient.sendWebChatMessage( - sessionType, - messageChain, - selectedPipelineId, - 120000, - ); + setMessages((prevMessages) => [...prevMessages, userMessage, botMessage]); + setInputValue(''); + setHasAt(false); - setMessages((prevMessages) => [...prevMessages, response.message]); + try { + let botMessageId = botMessage.id; + let accumulatedContent = ''; + + await httpClient.sendStreamingWebChatMessage( + sessionType, + messageChain, + selectedPipelineId, + (data) => { + // 处理流式响应数据 + if (data.message) { + accumulatedContent += data.message; + + // 更新bot消息 + setMessages((prevMessages) => { + const updatedMessages = [...prevMessages]; + const botMessageIndex = updatedMessages.findIndex( + (msg) => msg.id === botMessageId && msg.role === 'assistant' + ); + + if (botMessageIndex !== -1) { + const updatedBotMessage = { + ...updatedMessages[botMessageIndex], + content: accumulatedContent, + message_chain: [{ type: 'Plain', text: accumulatedContent }], + }; + updatedMessages[botMessageIndex] = updatedBotMessage; + } + + return updatedMessages; + }); + } + }, + () => { + // 流传输完成 + console.log('Streaming completed'); + }, + (error) => { + // 处理错误 + console.error('Streaming error:', error); + if (sessionType === 'person') { + toast.error(t('pipelines.debugDialog.sendFailed')); + } + } + ); + } catch (error) { + console.error('Failed to send streaming message:', error); + if (sessionType === 'person') { + toast.error(t('pipelines.debugDialog.sendFailed')); + } + } + } else { + + setMessages((prevMessages) => [...prevMessages, userMessage]); + setInputValue(''); + setHasAt(false); + + const response = await httpClient.sendWebChatMessage( + sessionType, + messageChain, + selectedPipelineId, + 120000, + ); + + setMessages((prevMessages) => [...prevMessages, response.message]); + } } catch ( // eslint-disable-next-line @typescript-eslint/no-explicit-any error: any @@ -306,6 +377,13 @@ export default function DebugDialog({
+
+ {t('pipelines.debugDialog.streaming')} + +
{hasAt && ( diff --git a/web/src/app/infra/http/HttpClient.ts b/web/src/app/infra/http/HttpClient.ts index 7c05bf09..3c04dc01 100644 --- a/web/src/app/infra/http/HttpClient.ts +++ b/web/src/app/infra/http/HttpClient.ts @@ -375,6 +375,89 @@ class HttpClient { ); } + public async sendStreamingWebChatMessage( + sessionType: string, + messageChain: object[], + pipelineId: string, + onMessage: (data: any) => void, + onComplete: () => void, + onError: (error: any) => void, + ): Promise { + try { + const url = `${this.baseURL}/api/v1/pipelines/${pipelineId}/chat/send`; + + // 使用fetch发送流式请求,因为axios在浏览器环境中不直接支持流式响应 + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + session_type: sessionType, + message: messageChain, + is_stream: true, + }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + if (!response.body) { + throw new Error('ReadableStream not supported'); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + + // 读取流式响应 + try { + while (true) { + const { done, value } = await reader.read(); + + if (done) { + onComplete(); + break; + } + + // 解码数据 + buffer += decoder.decode(value, { stream: true }); + + // 处理完整的JSON对象 + const lines = buffer.split('\n\n'); + buffer = lines.pop() || ''; + + for (const line of lines) { + if (line.startsWith('data: ')) { + try { + const data = JSON.parse(line.slice(6)); + + if (data.type === 'end') { + // 流传输结束 + reader.cancel(); + onComplete(); + return; + } + + if (data.message) { + // 处理消息数据 + onMessage(data); + } + } catch (error) { + console.error('Error parsing streaming data:', error); + } + } + } + } + } finally { + reader.releaseLock(); + } + } catch (error) { + onError(error); + } + } + public getWebChatHistoryMessages( pipelineId: string, sessionType: string, diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts index 7c306d51..2da16025 100644 --- a/web/src/i18n/locales/en-US.ts +++ b/web/src/i18n/locales/en-US.ts @@ -233,6 +233,7 @@ const enUS = { loadMessagesFailed: 'Failed to load messages', loadPipelinesFailed: 'Failed to load pipelines', atTips: 'Mention the bot', + streaming: 'Streaming', }, }, knowledge: { diff --git a/web/src/i18n/locales/ja-JP.ts b/web/src/i18n/locales/ja-JP.ts index bdd6374d..03ec8398 100644 --- a/web/src/i18n/locales/ja-JP.ts +++ b/web/src/i18n/locales/ja-JP.ts @@ -235,6 +235,7 @@ const jaJP = { loadMessagesFailed: 'メッセージの読み込みに失敗しました', loadPipelinesFailed: 'パイプラインの読み込みに失敗しました', atTips: 'ボットをメンション', + streaming: 'ストリーミング', }, }, knowledge: { diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index 5209c5e2..cb156e46 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -228,6 +228,7 @@ const zhHans = { loadMessagesFailed: '加载消息失败', loadPipelinesFailed: '加载流水线失败', atTips: '提及机器人', + streaming: '流式传输', }, }, knowledge: {