feat(json-render): 修复 markdown JSON 提取和渲染时序问题
- 添加 extractJsonFromMarkdown 函数从 response 提取 JSON - 修复时序问题:specs 更新后自动更新最后一条消息 - 修复循环依赖:移除不必要依赖项 - 小说列表卡片正确渲染(10个卡片) 测试验证:移动端模式下,"获取小说列表"成功渲染 10 个小说卡片 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user