feat(前端): 流式聊天支持 Pyodide 图形渲染
useStreamChat.ts: - 处理 pyodide_execution_required 事件触发浏览器端执行 - 处理 tool_execution_result 事件接收服务端执行结果 - 添加 Pyodide 加载状态管理和进度显示 - 实现图片数据保存到数据库 - ChatMessage 类型增加 images 和 pyodideStatus 属性 page.tsx: - 从数据库加载历史消息的图片数据 - 传递 images 和 pyodideStatus 到 MessageBubble 组件
This commit is contained in:
parent
e5c5593686
commit
7d8a6a6939
@ -66,6 +66,8 @@ export default function ChatPage({ params }: PageProps) {
|
||||
status: 'completed' as const,
|
||||
inputTokens: msg.inputTokens || undefined,
|
||||
outputTokens: msg.outputTokens || undefined,
|
||||
// 从数据库加载图片数据
|
||||
images: (msg.images as string[]) || undefined,
|
||||
}));
|
||||
setInitialMessages(historyMessages);
|
||||
}
|
||||
@ -272,6 +274,8 @@ export default function ChatPage({ params }: PageProps) {
|
||||
thinkingContent={message.thinkingContent}
|
||||
isStreaming={message.status === 'streaming'}
|
||||
error={message.error}
|
||||
images={message.images}
|
||||
pyodideStatus={message.pyodideStatus}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback, useRef } from 'react';
|
||||
import { executePythonInPyodide, type LoadingCallback } from '@/services/tools/pyodideRunner';
|
||||
|
||||
export interface StreamMessage {
|
||||
type: 'thinking' | 'text' | 'tool_use_start' | 'done' | 'error';
|
||||
type: 'thinking' | 'text' | 'tool_use_start' | 'tool_execution_result' | 'pyodide_execution_required' | 'done' | 'error';
|
||||
content?: string;
|
||||
id?: string;
|
||||
name?: string;
|
||||
@ -11,6 +12,13 @@ export interface StreamMessage {
|
||||
inputTokens?: number;
|
||||
outputTokens?: number;
|
||||
error?: string;
|
||||
// Pyodide 执行相关
|
||||
code?: string;
|
||||
language?: string;
|
||||
// 工具执行结果
|
||||
success?: boolean;
|
||||
result?: string;
|
||||
images?: string[];
|
||||
}
|
||||
|
||||
export interface ChatMessage {
|
||||
@ -22,6 +30,37 @@ export interface ChatMessage {
|
||||
error?: string;
|
||||
inputTokens?: number;
|
||||
outputTokens?: number;
|
||||
// 工具执行产生的图片
|
||||
images?: string[];
|
||||
// Pyodide 加载状态
|
||||
pyodideStatus?: {
|
||||
stage: 'loading' | 'ready' | 'error';
|
||||
message: string;
|
||||
progress?: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新消息图片到数据库
|
||||
*/
|
||||
async function saveMessageImages(messageId: string, images: string[]): Promise<void> {
|
||||
if (!messageId || !images || images.length === 0) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/messages/${messageId}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ images }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('Failed to save message images:', await response.text());
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving message images:', error);
|
||||
}
|
||||
}
|
||||
|
||||
export function useStreamChat() {
|
||||
@ -29,6 +68,8 @@ export function useStreamChat() {
|
||||
const [isStreaming, setIsStreaming] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
// 临时存储 Pyodide 执行产生的图片,等待 messageId
|
||||
const pendingImagesRef = useRef<string[]>([]);
|
||||
|
||||
// 发送消息
|
||||
const sendMessage = useCallback(async (options: {
|
||||
@ -137,7 +178,95 @@ export function useStreamChat() {
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
} else if (event.type === 'tool_execution_result') {
|
||||
// 处理工具执行结果(包括图片)
|
||||
if (event.images && event.images.length > 0) {
|
||||
setMessages((prev) => {
|
||||
const updated = [...prev];
|
||||
const lastIndex = updated.length - 1;
|
||||
if (updated[lastIndex]?.role === 'assistant') {
|
||||
const existingImages = updated[lastIndex].images || [];
|
||||
updated[lastIndex] = {
|
||||
...updated[lastIndex],
|
||||
images: [...existingImages, ...event.images!],
|
||||
};
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
}
|
||||
} else if (event.type === 'pyodide_execution_required') {
|
||||
// 需要在浏览器端执行 Python 图形代码
|
||||
const code = event.code || '';
|
||||
|
||||
// 更新 Pyodide 加载状态
|
||||
const updatePyodideStatus: LoadingCallback = (status) => {
|
||||
setMessages((prev) => {
|
||||
const updated = [...prev];
|
||||
const lastIndex = updated.length - 1;
|
||||
if (updated[lastIndex]?.role === 'assistant') {
|
||||
updated[lastIndex] = {
|
||||
...updated[lastIndex],
|
||||
pyodideStatus: status,
|
||||
};
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
|
||||
// 执行 Pyodide
|
||||
try {
|
||||
const result = await executePythonInPyodide(code, updatePyodideStatus);
|
||||
|
||||
// 添加执行结果文本
|
||||
const resultText = result.success
|
||||
? `\n\n✅ Python [Pyodide] 代码执行完成 (${result.executionTime}ms)${result.images.length > 0 ? `,生成 ${result.images.length} 张图表` : ''}\n\n`
|
||||
: `\n\n❌ Python 代码执行失败: ${result.error}\n\n`;
|
||||
|
||||
fullContent += resultText;
|
||||
|
||||
// 将图片存入临时变量,等待 messageId 后保存到数据库
|
||||
if (result.images && result.images.length > 0) {
|
||||
pendingImagesRef.current = [...pendingImagesRef.current, ...result.images];
|
||||
}
|
||||
|
||||
// 更新消息,添加图片
|
||||
setMessages((prev) => {
|
||||
const updated = [...prev];
|
||||
const lastIndex = updated.length - 1;
|
||||
if (updated[lastIndex]?.role === 'assistant') {
|
||||
const existingImages = updated[lastIndex].images || [];
|
||||
updated[lastIndex] = {
|
||||
...updated[lastIndex],
|
||||
content: fullContent,
|
||||
images: [...existingImages, ...result.images],
|
||||
pyodideStatus: undefined, // 清除加载状态
|
||||
};
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
} catch (pyodideError) {
|
||||
const errorMsg = pyodideError instanceof Error ? pyodideError.message : '未知错误';
|
||||
fullContent += `\n\n❌ Pyodide 执行错误: ${errorMsg}\n\n`;
|
||||
setMessages((prev) => {
|
||||
const updated = [...prev];
|
||||
const lastIndex = updated.length - 1;
|
||||
if (updated[lastIndex]?.role === 'assistant') {
|
||||
updated[lastIndex] = {
|
||||
...updated[lastIndex],
|
||||
content: fullContent,
|
||||
pyodideStatus: { stage: 'error', message: errorMsg },
|
||||
};
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
}
|
||||
} else if (event.type === 'done') {
|
||||
// 如果有待保存的图片,保存到数据库
|
||||
if (event.messageId && pendingImagesRef.current.length > 0) {
|
||||
saveMessageImages(event.messageId, pendingImagesRef.current);
|
||||
pendingImagesRef.current = []; // 清空临时存储
|
||||
}
|
||||
|
||||
setMessages((prev) => {
|
||||
const updated = [...prev];
|
||||
const lastIndex = updated.length - 1;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user