diff --git a/src/app/chat/[id]/page.tsx b/src/app/chat/[id]/page.tsx index a3d3227..4e2646f 100644 --- a/src/app/chat/[id]/page.tsx +++ b/src/app/chat/[id]/page.tsx @@ -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} /> )) )} diff --git a/src/hooks/useStreamChat.ts b/src/hooks/useStreamChat.ts index 65d6018..629b575 100644 --- a/src/hooks/useStreamChat.ts +++ b/src/hooks/useStreamChat.ts @@ -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 { + 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(null); const abortControllerRef = useRef(null); + // 临时存储 Pyodide 执行产生的图片,等待 messageId + const pendingImagesRef = useRef([]); // 发送消息 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;