mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-27 07:54:19 +00:00
feat:webchat frontend stream
This commit is contained in:
@@ -10,6 +10,7 @@ import { cn } from '@/lib/utils';
|
|||||||
import { Message } from '@/app/infra/entities/message';
|
import { Message } from '@/app/infra/entities/message';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import AtBadge from './AtBadge';
|
import AtBadge from './AtBadge';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
|
||||||
interface MessageComponent {
|
interface MessageComponent {
|
||||||
type: 'At' | 'Plain';
|
type: 'At' | 'Plain';
|
||||||
@@ -36,6 +37,7 @@ export default function DebugDialog({
|
|||||||
const [showAtPopover, setShowAtPopover] = useState(false);
|
const [showAtPopover, setShowAtPopover] = useState(false);
|
||||||
const [hasAt, setHasAt] = useState(false);
|
const [hasAt, setHasAt] = useState(false);
|
||||||
const [isHovering, setIsHovering] = useState(false);
|
const [isHovering, setIsHovering] = useState(false);
|
||||||
|
const [isStreaming, setIsStreaming] = useState(false);
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const popoverRef = useRef<HTMLDivElement>(null);
|
const popoverRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -157,27 +159,96 @@ export default function DebugDialog({
|
|||||||
// for showing
|
// for showing
|
||||||
text_content = '@webchatbot' + text_content;
|
text_content = '@webchatbot' + text_content;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userMessage: Message = {
|
const userMessage: Message = {
|
||||||
id: -1,
|
id: -1,
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: text_content,
|
content: text_content,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
message_chain: messageChain,
|
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]);
|
// 添加用户消息和初始bot消息到状态
|
||||||
setInputValue('');
|
|
||||||
setHasAt(false);
|
|
||||||
|
|
||||||
const response = await httpClient.sendWebChatMessage(
|
setMessages((prevMessages) => [...prevMessages, userMessage, botMessage]);
|
||||||
sessionType,
|
setInputValue('');
|
||||||
messageChain,
|
setHasAt(false);
|
||||||
selectedPipelineId,
|
|
||||||
120000,
|
|
||||||
);
|
|
||||||
|
|
||||||
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 (
|
} catch (
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
error: any
|
error: any
|
||||||
@@ -306,6 +377,13 @@ export default function DebugDialog({
|
|||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|
||||||
<div className="p-4 pb-0 bg-white flex gap-2">
|
<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">
|
<div className="flex-1 flex items-center gap-2">
|
||||||
{hasAt && (
|
{hasAt && (
|
||||||
<AtBadge targetName="webchatbot" onRemove={handleAtRemove} />
|
<AtBadge targetName="webchatbot" onRemove={handleAtRemove} />
|
||||||
|
|||||||
@@ -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(
|
public getWebChatHistoryMessages(
|
||||||
pipelineId: string,
|
pipelineId: string,
|
||||||
sessionType: string,
|
sessionType: string,
|
||||||
|
|||||||
@@ -233,6 +233,7 @@ const enUS = {
|
|||||||
loadMessagesFailed: 'Failed to load messages',
|
loadMessagesFailed: 'Failed to load messages',
|
||||||
loadPipelinesFailed: 'Failed to load pipelines',
|
loadPipelinesFailed: 'Failed to load pipelines',
|
||||||
atTips: 'Mention the bot',
|
atTips: 'Mention the bot',
|
||||||
|
streaming: 'Streaming',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
knowledge: {
|
knowledge: {
|
||||||
|
|||||||
@@ -235,6 +235,7 @@ const jaJP = {
|
|||||||
loadMessagesFailed: 'メッセージの読み込みに失敗しました',
|
loadMessagesFailed: 'メッセージの読み込みに失敗しました',
|
||||||
loadPipelinesFailed: 'パイプラインの読み込みに失敗しました',
|
loadPipelinesFailed: 'パイプラインの読み込みに失敗しました',
|
||||||
atTips: 'ボットをメンション',
|
atTips: 'ボットをメンション',
|
||||||
|
streaming: 'ストリーミング',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
knowledge: {
|
knowledge: {
|
||||||
|
|||||||
@@ -228,6 +228,7 @@ const zhHans = {
|
|||||||
loadMessagesFailed: '加载消息失败',
|
loadMessagesFailed: '加载消息失败',
|
||||||
loadPipelinesFailed: '加载流水线失败',
|
loadPipelinesFailed: '加载流水线失败',
|
||||||
atTips: '提及机器人',
|
atTips: '提及机器人',
|
||||||
|
streaming: '流式传输',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
knowledge: {
|
knowledge: {
|
||||||
|
|||||||
Reference in New Issue
Block a user