|
|
@@ -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);
|