feat:webchat frontend stream

This commit is contained in:
fdc
2025-07-31 09:51:25 +08:00
committed by Junyan Qin
parent 2a17e89a99
commit 00a8410c94
5 changed files with 181 additions and 17 deletions

View File

@@ -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<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const popoverRef = useRef<HTMLDivElement>(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({
</ScrollArea>
<div className="p-4 pb-0 bg-white flex gap-2">
<div className="flex items-center gap-2">
<span className="text-sm text-gray-600">{t('pipelines.debugDialog.streaming')}</span>
<Switch
checked={isStreaming}
onCheckedChange={setIsStreaming}
/>
</div>
<div className="flex-1 flex items-center gap-2">
{hasAt && (
<AtBadge targetName="webchatbot" onRemove={handleAtRemove} />

View File

@@ -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<void> {
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,

View File

@@ -233,6 +233,7 @@ const enUS = {
loadMessagesFailed: 'Failed to load messages',
loadPipelinesFailed: 'Failed to load pipelines',
atTips: 'Mention the bot',
streaming: 'Streaming',
},
},
knowledge: {

View File

@@ -235,6 +235,7 @@ const jaJP = {
loadMessagesFailed: 'メッセージの読み込みに失敗しました',
loadPipelinesFailed: 'パイプラインの読み込みに失敗しました',
atTips: 'ボットをメンション',
streaming: 'ストリーミング',
},
},
knowledge: {

View File

@@ -228,6 +228,7 @@ const zhHans = {
loadMessagesFailed: '加载消息失败',
loadPipelinesFailed: '加载流水线失败',
atTips: '提及机器人',
streaming: '流式传输',
},
},
knowledge: {