diff --git a/frontend-v2/app/page.tsx b/frontend-v2/app/page.tsx index 5035d2c..d9caa5a 100644 --- a/frontend-v2/app/page.tsx +++ b/frontend-v2/app/page.tsx @@ -41,8 +41,9 @@ export default function Home() { toolCallsCount: toolCalls?.length || 0, hasError: !!error, }); + + // 情况 1: 添加新消息(加载完成且有待处理消息) if (!isLoading && pendingMessageRef.current) { - // 即使没有响应内容,也添加消息(可能只有工具调用结果) const assistantMessage = { role: 'assistant' as const, content: response || (error ? `⚠️ ${error}` : ''), @@ -53,6 +54,24 @@ export default function Home() { setHistory((prev) => [...prev, assistantMessage]); pendingMessageRef.current = null; } + // 情况 2: 更新最后一条消息(specs 从 response 中提取后更新) + // 这解决了 specs 在消息添加后才更新的时序问题 + else if (!isLoading && specs && specs.length > 0) { + setHistory((prev) => { + const lastMessage = prev[prev.length - 1]; + // 如果最后一条是 assistant 消息且没有 specs,更新它 + if (lastMessage && lastMessage.role === 'assistant' && !lastMessage.specs) { + console.log('[useEffect] Updating last message with specs:', specs); + const updated = [...prev]; + updated[prev.length - 1] = { + ...lastMessage, + specs: specs, + }; + return updated; + } + return prev; + }); + } }, [isLoading, response, toolCalls, specs, error]); const handleSend = async (message: string) => { diff --git a/frontend-v2/lib/hooks.ts b/frontend-v2/lib/hooks.ts index 74f5e50..369c9e8 100644 --- a/frontend-v2/lib/hooks.ts +++ b/frontend-v2/lib/hooks.ts @@ -39,30 +39,147 @@ export function useChat() { console.log('[useEffect] toolCalls updated:', toolCalls); }, [toolCalls]); - // 监控 response 状态变化 + // 监控 response 状态变化,并在完成时提取 JSON spec useEffect(() => { console.log('[useEffect] response updated:', { length: response.length, preview: response.substring(0, 100), }); - }, [response]); + + // 只在加载完成且有响应内容时尝试提取 spec + // 这确保在流式输出完成后,从 response 中提取 JSON + if (!isLoading && response && response.trim()) { + console.log('[useEffect] Loading complete, extracting JSON from response...'); + const extractedData = extractJsonFromMarkdown(response); + console.log('[useEffect] Extracted data:', extractedData); + + if (extractedData) { + const newSpecs: any[] = []; + + // 检查是否为有效的组件 spec(有 type 字段) + if (extractedData.type && typeof extractedData.type === 'string') { + newSpecs.push(extractedData); + console.log('[useEffect] Added spec from response:', extractedData); + } + // 如果是数组,检查每个元素 + else if (Array.isArray(extractedData)) { + for (const item of extractedData) { + if (item && item.type && typeof item.type === 'string') { + newSpecs.push(item); + console.log('[useEffect] Added spec from response array:', item); + } + } + } + + if (newSpecs.length > 0) { + console.log('[useEffect] Setting specs from response:', newSpecs); + setSpecs(newSpecs); + } + } + } + }, [response, isLoading]); // extractJsonFromMarkdown 依赖为空数组,不需要在依赖项中 + + /** + * 从 markdown 代码块中提取 JSON + * 支持格式:```json ... ``` 或 ``` ... ``` + */ + const extractJsonFromMarkdown = useCallback((text: string): any | null => { + if (typeof text !== 'string') { + return null; + } + + // 1. 匹配 ```json ... ``` 代码块(先提取整个代码块内容,不限制格式) + const jsonCodeBlockMatch = text.match(/```json\s*([\s\S]*?)\s*```/); + if (jsonCodeBlockMatch) { + try { + const jsonStr = jsonCodeBlockMatch[1].trim(); + console.log('[extractJsonFromMarkdown] Found json code block, parsing:', jsonStr.substring(0, 100)); + const parsed = JSON.parse(jsonStr); + console.log('[extractJsonFromMarkdown] Parsed successfully:', parsed); + return parsed; + } catch (e) { + console.error('[extractJsonFromMarkdown] Failed to parse JSON code block:', e); + // 继续尝试其他方式 + } + } + + // 2. 匹配 ``` ... ``` 代码块(无语言标识) + const codeBlockMatch = text.match(/```\s*([\s\S]*?)\s*```/); + if (codeBlockMatch) { + try { + const jsonStr = codeBlockMatch[1].trim(); + console.log('[extractJsonFromMarkdown] Found code block, trying to parse:', jsonStr.substring(0, 100)); + const parsed = JSON.parse(jsonStr); + console.log('[extractJsonFromMarkdown] Parsed successfully:', parsed); + return parsed; + } catch (e) { + console.log('[extractJsonFromMarkdown] Code block is not JSON, continuing...'); + // 不是 JSON,继续尝试其他方式 + } + } + + // 3. 尝试直接解析整个文本 + try { + console.log('[extractJsonFromMarkdown] Trying to parse entire text as JSON'); + const parsed = JSON.parse(text); + console.log('[extractJsonFromMarkdown] Parsed successfully:', parsed); + return parsed; + } catch { + console.log('[extractJsonFromMarkdown] Failed to parse as JSON'); + return null; + } + }, []); // 生成 specs 的通用函数 const generateSpecs = useCallback(() => { const currentToolCalls = toolCallsRef.current; console.log('[generateSpecs] Current toolCalls:', currentToolCalls); + console.log('[generateSpecs] Current response:', response?.substring(0, 200)); + + const newSpecs: any[] = []; + + // 1. 从 toolCalls 生成 specs(原有逻辑) if (currentToolCalls.length > 0) { const { specFromToolCall } = require('@/lib/json-render-catalog'); - const newSpecs = currentToolCalls.map((call) => specFromToolCall(call.tool, call.result)).filter(Boolean); - console.log('[generateSpecs] Generated specs:', newSpecs); + const toolSpecs = currentToolCalls.map((call) => specFromToolCall(call.tool, call.result)).filter(Boolean); + newSpecs.push(...toolSpecs); + console.log('[generateSpecs] Generated specs from toolCalls:', toolSpecs); + } + + // 2. 从 response 中提取 JSON 并生成 specs(新增逻辑) + if (response && response.trim()) { + const extractedData = extractJsonFromMarkdown(response); + console.log('[generateSpecs] Extracted JSON from response:', extractedData); + + if (extractedData) { + // 检查是否为有效的组件 spec(有 type 字段) + if (extractedData.type && typeof extractedData.type === 'string') { + newSpecs.push(extractedData); + console.log('[generateSpecs] Added spec from response:', extractedData); + } + // 如果是数组,检查每个元素 + else if (Array.isArray(extractedData)) { + for (const item of extractedData) { + if (item && item.type && typeof item.type === 'string') { + newSpecs.push(item); + console.log('[generateSpecs] Added spec from response array:', item); + } + } + } + } + } + + if (newSpecs.length > 0) { + console.log('[generateSpecs] Setting specs:', newSpecs); setSpecs(newSpecs); } + // 使用 setTimeout 确保 specs 状态更新先被处理 setTimeout(() => { setIsLoading(false); console.log('[generateSpecs] Set isLoading to false'); }, 0); - }, []); + }, [response]); // extractJsonFromMarkdown 依赖为空数组,不需要在依赖项中 const sendMessage = useCallback(async (message: string, history: ChatMessage[] = []) => { setIsLoading(true);