修复思维导图实时渲染问题

This commit is contained in:
lixinran 2025-09-08 18:20:48 +08:00
parent 7b6601e010
commit fd1b71dd75
8 changed files with 636 additions and 64 deletions

View File

@ -103,6 +103,44 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# CORS settings # CORS settings
CORS_ALLOW_ALL_ORIGINS = True CORS_ALLOW_ALL_ORIGINS = True
CORS_ALLOW_CREDENTIALS = True CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_HEADERS = [
'accept',
'accept-encoding',
'authorization',
'content-type',
'dnt',
'origin',
'user-agent',
'x-csrftoken',
'x-requested-with',
]
CORS_ALLOW_METHODS = [
'DELETE',
'GET',
'OPTIONS',
'PATCH',
'POST',
'PUT',
]
# REST Framework settings
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
],
'DEFAULT_PARSER_CLASSES': [
'rest_framework.parsers.JSONParser',
'rest_framework.parsers.MultiPartParser',
'rest_framework.parsers.FormParser',
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 100,
}
# File upload settings
MAX_UPLOAD_SIZE = 16 * 1024 * 1024 # 16MB
ALLOWED_FILE_TYPES = ['.docx', '.pdf', '.txt', '.md']
# REST Framework settings # REST Framework settings
REST_FRAMEWORK = { REST_FRAMEWORK = {

Binary file not shown.

View File

@ -2,7 +2,7 @@ import re
import traceback import traceback
import json import json
def call_ai_api(system_prompt, user_prompt, model="glm-4.5", base_url="https://open.bigmodel.cn/api/paas/v4/", api_key="ce39bdd4fcf34ec0aec75072bc9ff988.hAp7HZTVUwy7vImn"): def call_ai_api(system_prompt, user_prompt, model="glm-4.5", base_url="https://open.bigmodel.cn/api/paas/v4/", api_key="ce39bdd4fcf34ec0aec75072bc9ff988.hAp7HZTVUwy7vImn", stream=False):
""" """
调用AI API生成Markdown 调用AI API生成Markdown
""" """
@ -12,6 +12,32 @@ def call_ai_api(system_prompt, user_prompt, model="glm-4.5", base_url="https://o
from openai import OpenAI from openai import OpenAI
except ImportError: except ImportError:
print("OpenAI库未安装,返回模拟数据") print("OpenAI库未安装,返回模拟数据")
if stream:
# 返回模拟流式数据
def mock_stream():
mock_content = f"""# {user_prompt}
## 概述
{user_prompt}是一个重要的概念和领域。
## 核心要素
- 要素1
- 要素2
- 要素3
## 应用场景
- 场景1
- 场景2
- 场景3
## 发展趋势
- 趋势1
- 趋势2
- 趋势3"""
for char in mock_content:
yield char
return mock_stream()
else:
return f"""# {user_prompt} return f"""# {user_prompt}
## 概述 ## 概述
@ -40,25 +66,44 @@ def call_ai_api(system_prompt, user_prompt, model="glm-4.5", base_url="https://o
print(f"发送AI API请求到: {base_url}") print(f"发送AI API请求到: {base_url}")
print(f"模型: {model}") print(f"模型: {model}")
print(f"流式模式: {stream}")
# 创建OpenAI客户端 # 创建OpenAI客户端
client = OpenAI(api_key=api_key, base_url=base_url) client = OpenAI(api_key=api_key, base_url=base_url)
# 使用非流式调用,更简单可靠 # 根据stream参数决定是否使用流式调用
try: try:
response = client.chat.completions.create( response = client.chat.completions.create(
model=model, model=model,
messages=messages, messages=messages,
temperature=0.7, temperature=0.7,
max_tokens=8000, # 增加token限制,避免内容截断 max_tokens=8000, # 增加token限制,避免内容截断
stream=False stream=stream
) )
except Exception as e: except Exception as e:
print(f"API调用失败: {e}") print(f"API调用失败: {e}")
# 如果API调用失败,抛出异常而不是返回模拟数据 # 如果API调用失败,抛出异常而不是返回模拟数据
raise Exception(f"AI API调用失败: {e}") raise Exception(f"AI API调用失败: {e}")
# 获取响应内容 if stream:
# 流式响应处理
def generate_stream():
full_content = ""
try:
for chunk in response:
if chunk.choices[0].delta.content is not None:
content_chunk = chunk.choices[0].delta.content
full_content += content_chunk
yield content_chunk
print(f"流式响应完成,总长度: {len(full_content)}")
except Exception as e:
print(f"流式响应处理失败: {e}")
raise e
return full_content
return generate_stream()
else:
# 非流式响应处理
content = response.choices[0].message.content content = response.choices[0].message.content
print(f"AI原始响应: {content}") print(f"AI原始响应: {content}")

View File

@ -11,4 +11,6 @@ urlpatterns = [
# AI接口 # AI接口
path('ai/generate-markdown', views_doc.generate_markdown), path('ai/generate-markdown', views_doc.generate_markdown),
path('ai/generate-stream', views_doc.generate_ai_content_stream),
path('ai/test-stream', views_doc.test_stream), # 添加测试端点
] ]

View File

@ -3,6 +3,8 @@ from rest_framework.response import Response
from rest_framework import status from rest_framework import status
from django.db import transaction from django.db import transaction
from django.utils import timezone from django.utils import timezone
from django.http import StreamingHttpResponse
import json
from .models import mindMap, Node from .models import mindMap, Node
from .serializers import map_mindmap_to_doc, map_node_to_doc from .serializers import map_mindmap_to_doc, map_node_to_doc
@ -468,3 +470,141 @@ def generate_markdown(request):
}, status=500) }, status=500)
@api_view(['POST', 'OPTIONS'])
def generate_ai_content_stream(request):
"""
流式生成AI内容
"""
# 处理OPTIONS请求(CORS预检请求)
if request.method == 'OPTIONS':
response = Response()
response['Access-Control-Allow-Origin'] = '*'
response['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
response['Access-Control-Allow-Headers'] = 'Content-Type, Authorization, Cache-Control, X-Requested-With'
response['Access-Control-Allow-Credentials'] = 'true'
response['Access-Control-Max-Age'] = '86400'
return response
try:
data = request.data
user_prompt = data.get('user_prompt', '')
system_prompt = data.get('system_prompt', '你是一个专业的思维导图内容生成助手,请根据用户的需求生成结构化的Markdown内容。')
model = data.get('model', 'glm-4.5')
base_url = data.get('base_url', 'https://open.bigmodel.cn/api/paas/v4/')
api_key = data.get('api_key', '')
if not user_prompt:
return Response({'error': '用户提示词不能为空'}, status=400)
# 导入AI服务
from .ai_service import call_ai_api
def generate_stream():
try:
print(f"开始调用流式AI API...")
# 调用流式AI API
stream = call_ai_api(system_prompt, user_prompt, model, base_url, api_key, stream=True)
if stream is None:
print("AI API返回None,发送错误信号")
yield f"data: {json.dumps({'type': 'error', 'content': 'AI API调用失败'})}\n\n"
return
print("开始发送流式数据...")
# 发送开始信号
yield f"data: {json.dumps({'type': 'start', 'content': ''})}\n\n"
# 发送流式内容
chunk_count = 0
for chunk in stream:
if chunk:
chunk_count += 1
print(f"发送第{chunk_count}个数据块: {chunk[:50]}...")
yield f"data: {json.dumps({'type': 'chunk', 'content': chunk})}\n\n"
print(f"流式数据发送完成,总共{chunk_count}个数据块")
# 发送结束信号
yield f"data: {json.dumps({'type': 'end', 'content': ''})}\n\n"
except Exception as e:
print(f"流式生成过程中发生错误: {e}")
import traceback
traceback.print_exc()
# 发送错误信号
yield f"data: {json.dumps({'type': 'error', 'content': str(e)})}\n\n"
response = StreamingHttpResponse(
generate_stream(),
content_type='text/event-stream'
)
# 修复CORS配置,移除不允许的头部
response['Cache-Control'] = 'no-cache'
response['Access-Control-Allow-Origin'] = '*'
response['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
response['Access-Control-Allow-Headers'] = 'Content-Type, Authorization, Cache-Control, X-Requested-With'
response['Access-Control-Allow-Credentials'] = 'true'
response['Access-Control-Max-Age'] = '86400'
return response
except Exception as e:
print(f"流式API处理过程中发生错误: {e}")
import traceback
traceback.print_exc()
return Response({
'error': str(e),
'success': False
}, status=500)
@api_view(['POST', 'OPTIONS'])
def test_stream(request):
"""
测试流式响应
"""
# 处理OPTIONS请求(CORS预检请求)
if request.method == 'OPTIONS':
response = Response()
response['Access-Control-Allow-Origin'] = '*'
response['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
response['Access-Control-Allow-Headers'] = 'Content-Type, Authorization, Cache-Control, X-Requested-With'
response['Access-Control-Allow-Credentials'] = 'true'
response['Access-Control-Max-Age'] = '86400'
return response
def generate_test_stream():
try:
# 发送开始信号
yield f"data: {json.dumps({'type': 'start', 'content': ''})}\n\n"
# 发送测试数据
test_content = "# 测试思维导图\n\n## 主要主题\n- 主题1\n- 主题2\n\n## 详细内容\n- 内容1\n- 内容2"
for i, char in enumerate(test_content):
yield f"data: {json.dumps({'type': 'chunk', 'content': char})}\n\n"
# 添加小延迟模拟流式效果
import time
time.sleep(0.01)
# 发送结束信号
yield f"data: {json.dumps({'type': 'end', 'content': ''})}\n\n"
except Exception as e:
yield f"data: {json.dumps({'type': 'error', 'content': str(e)})}\n\n"
response = StreamingHttpResponse(
generate_test_stream(),
content_type='text/event-stream'
)
# 修复CORS配置,移除不允许的头部
response['Cache-Control'] = 'no-cache'
response['Access-Control-Allow-Origin'] = '*'
response['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
response['Access-Control-Allow-Headers'] = 'Content-Type, Authorization, Cache-Control, X-Requested-With'
response['Access-Control-Allow-Credentials'] = 'true'
response['Access-Control-Max-Age'] = '86400'
return response

View File

@ -1,7 +1,7 @@
<template> <template>
<div id="app"> <div id="app">
<!-- AI侧边栏 --> <!-- AI侧边栏 -->
<AISidebar /> <AISidebar @start-realtime-generation="handleStartRealtimeGeneration" />
<!-- 主内容区域 --> <!-- 主内容区域 -->
<div class="main-content"> <div class="main-content">
@ -11,8 +11,19 @@
</template> </template>
<script setup> <script setup>
import { ref } from 'vue';
import MindMap from "./components/MindMap.vue"; import MindMap from "./components/MindMap.vue";
import AISidebar from "./components/AISidebar.vue"; import AISidebar from "./components/AISidebar.vue";
const mindMapRef = ref(null);
// 处理开始实时生成事件
const handleStartRealtimeGeneration = () => {
console.log('🎬 开始实时生成,切换到思维导图界面');
if (mindMapRef.value) {
mindMapRef.value.showMindMapPage();
}
};
</script> </script>
<style> <style>

View File

@ -171,6 +171,9 @@ import { ref, reactive } from 'vue';
import axios from 'axios'; import axios from 'axios';
import { marked } from 'marked'; import { marked } from 'marked';
// 定义emit事件
const emit = defineEmits(['start-realtime-generation']);
// 响应式数据 // 响应式数据
const isCollapsed = ref(false); // 默认展开状态 const isCollapsed = ref(false); // 默认展开状态
const aiPrompt = ref(''); const aiPrompt = ref('');
@ -277,26 +280,35 @@ const generateMarkdownFromFile = async () => {
isGenerating.value = true; isGenerating.value = true;
// 立即切换到思维导图界面,开始实时渲染
emit('start-realtime-generation');
try { try {
// 读取文件内容 // 读取文件内容
const fileContent = await readFileContent(uploadedFile.value); const fileContent = await readFileContent(uploadedFile.value);
console.log('📄 文件内容预览:', fileContent.substring(0, 200) + '...'); console.log('📄 文件内容预览:', fileContent.substring(0, 200) + '...');
// 调用AI API生成Markdown // 清空之前的内容,准备流式生成
markdownContent.value = '';
// 调用流式AI API生成Markdown
const systemPrompt = '你是一个专业的文档分析专家。请分析上传的文档内容,生成结构化的Markdown格式思维导图。要求:1. 提取主要主题和关键概念 2. 组织成层次分明的结构 3. 使用清晰的标题和子标题 4. 保持内容的逻辑性和完整性'; const systemPrompt = '你是一个专业的文档分析专家。请分析上传的文档内容,生成结构化的Markdown格式思维导图。要求:1. 提取主要主题和关键概念 2. 组织成层次分明的结构 3. 使用清晰的标题和子标题 4. 保持内容的逻辑性和完整性';
const userPrompt = `请分析以下文档内容并生成结构化Markdown:\n\n${fileContent}`; const userPrompt = `请分析以下文档内容并生成结构化Markdown:\n\n${fileContent}`;
console.log('🤖 系统提示词:', systemPrompt); console.log('🤖 系统提示词:', systemPrompt);
console.log('👤 用户提示词预览:', userPrompt.substring(0, 200) + '...'); console.log('👤 用户提示词预览:', userPrompt.substring(0, 200) + '...');
const markdown = await callAIMarkdownAPI(systemPrompt, userPrompt); // 关键修改:使用流式API而不是非流式API
await callAIStreamAPI(systemPrompt, userPrompt);
markdownContent.value = markdown; console.log('📝 流式生成Markdown完成!内容长度:', markdownContent.value.length, '字符');
console.log('📝 生成Markdown成功!内容长度:', markdown.length, '字符');
// 自动转换为JSON // 自动转换为JSON
await convertToJSON(); await convertToJSON();
// 添加到历史记录
addToHistory('AI生成: ' + uploadedFile.value.name, markdownContent.value);
showNotification('AI生成Markdown成功!正在自动保存...', 'success'); showNotification('AI生成Markdown成功!正在自动保存...', 'success');
// 延迟一下让用户看到成功消息,然后自动保存 // 延迟一下让用户看到成功消息,然后自动保存
@ -405,7 +417,7 @@ const extractPDFContent = async (file) => {
} }
}; };
// AI生成Markdown // AI生成Markdown(流式版本)
const generateMarkdown = async () => { const generateMarkdown = async () => {
if (!aiPrompt.value.trim()) { if (!aiPrompt.value.trim()) {
showNotification('请输入主题描述', 'error'); showNotification('请输入主题描述', 'error');
@ -416,19 +428,22 @@ const generateMarkdown = async () => {
savedAiPrompt.value = aiPrompt.value; savedAiPrompt.value = aiPrompt.value;
isGenerating.value = true; isGenerating.value = true;
try { markdownContent.value = ''; // 清空之前的内容
// 调用真实的AI API
const markdown = await callAIMarkdownAPI(null, aiPrompt.value);
markdownContent.value = markdown;
savedMarkdownContent.value = markdown;
console.log('📝 生成Markdown成功!内容长度:', markdown.length, '字符'); // 立即切换到思维导图界面,开始实时渲染
emit('start-realtime-generation');
try {
// 调用流式AI API
await callAIStreamAPI(null, aiPrompt.value);
console.log('📝 流式生成Markdown完成!内容长度:', markdownContent.value.length, '字符');
// 自动转换为JSON // 自动转换为JSON
await convertToJSON(); await convertToJSON();
// 添加到历史记录 // 添加到历史记录
addToHistory('AI生成: ' + aiPrompt.value.substring(0, 30) + '...', markdown); addToHistory('AI生成: ' + aiPrompt.value.substring(0, 30) + '...', markdownContent.value);
// 确保AI提示词不被清空 // 确保AI提示词不被清空
if (aiPrompt.value !== savedAiPrompt.value) { if (aiPrompt.value !== savedAiPrompt.value) {
@ -456,6 +471,138 @@ const generateMarkdown = async () => {
}; };
// 调用AI API生成Markdown // 调用AI API生成Markdown
// 流式AI API调用
const callAIStreamAPI = async (systemPrompt, userPrompt) => {
const defaultSystemPrompt = `你是一位Markdown格式转换专家。你的任务是将用户提供的文章内容精确转换为结构化的Markdown格式。请遵循以下步骤:
提取主标题: 识别文章最顶层的主标题(通常为文章题目或书名),并使用Markdown的 # 级别表示。
识别层级标题: 从文章内容中提取所有层级的内容标题(从主标题后的第一个标题开始,Level 1 至 Level 4)。判断层级依据:
视觉与结构特征: 如独立成行/段、位置(行首)、格式(加粗、编号如 1., 1.1, (1), - 等)。
语义逻辑: 标题之间的包含和并列关系。
在Markdown中,使用相应标题级别:
Level 1 标题用 ##
Level 2 标题用 ###
Level 3 标题用 ####
Level 4 标题用 #####
精确保留原文标题文字,不得修改、概括或润色。
处理正文内容: 对于每个标题下的正文内容区块(从该标题后开始,直到下一个同级或更高级别标题前):
直接保留原文文本,但根据内容结构适当格式化为Markdown。
如果内容是列表(如项目符号或编号列表),使用Markdown列表语法(例如 - 用于无序列表,1. 用于有序列表)。
保持段落和换行不变。
输出格式: 输出必须是纯Markdown格式的文本,不得包含任何额外说明、JSON或非Markdown元素。确保输出与示例风格一致。`;
const finalSystemPrompt = systemPrompt || defaultSystemPrompt;
const finalUserPrompt = userPrompt || `请将以下内容转换为结构化的Markdown格式:`;
try {
console.log('🚀 开始流式调用AI API...');
console.log('📋 系统提示词长度:', finalSystemPrompt.length);
console.log('👤 用户提示词长度:', finalUserPrompt.length);
const response = await fetch('http://127.0.0.1:8000/api/ai/generate-stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
system_prompt: finalSystemPrompt,
user_prompt: finalUserPrompt,
model: 'glm-4.5',
base_url: 'https://open.bigmodel.cn/api/paas/v4/',
api_key: 'ce39bdd4fcf34ec0aec75072bc9ff988.hAp7HZTVUwy7vImn'
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let chunkCount = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6));
if (data.type === 'start') {
console.log('🎬 开始接收流式数据...');
showNotification('AI开始生成内容...', 'info');
} else if (data.type === 'chunk') {
chunkCount++;
console.log(`📦 收到第${chunkCount}个数据块:`, data.content);
// 实时更新Markdown内容
markdownContent.value += data.content;
console.log('📝 当前Markdown内容长度:', markdownContent.value.length);
console.log('📝 当前Markdown内容预览:', markdownContent.value.substring(0, 100) + '...');
// 实时转换为JSON并更新显示
try {
const tempJSON = markdownToJSON(markdownContent.value);
convertedJSON.value = JSON.stringify(tempJSON, null, 2);
console.log('🔄 JSON转换成功,节点数量:', countNodes(tempJSON));
console.log('🔄 JSON结构预览:', JSON.stringify(tempJSON, null, 2).substring(0, 200) + '...');
// 🎯 关键:实时更新思维导图显示
console.log(' 发送实时更新事件到MindMap组件...');
window.dispatchEvent(new CustomEvent('realtime-mindmap-update', {
detail: {
data: tempJSON,
title: tempJSON.topic || 'AI生成中...',
source: 'ai-streaming',
chunkCount: chunkCount
}
}));
console.log('✅ 实时更新事件已发送');
} catch (e) {
// 忽略转换错误,继续接收数据
console.warn('⚠️ 实时转换JSON失败:', e);
console.warn('⚠️ 当前Markdown内容:', markdownContent.value);
}
} else if (data.type === 'end') {
console.log('✅ 流式数据接收完成,总共收到', chunkCount, '个数据块');
showNotification('AI内容生成完成!', 'success');
} else if (data.type === 'error') {
throw new Error(data.content);
}
} catch (e) {
console.warn('解析流式数据失败:', e);
}
}
}
}
} catch (error) {
console.error('流式AI API调用失败:', error);
throw error;
}
};
const callAIMarkdownAPI = async (systemPrompt, userPrompt, retryCount = 0) => { const callAIMarkdownAPI = async (systemPrompt, userPrompt, retryCount = 0) => {
const defaultSystemPrompt = `你是一位Markdown格式转换专家。你的任务是将用户提供的文章内容精确转换为结构化的Markdown格式。请遵循以下步骤: const defaultSystemPrompt = `你是一位Markdown格式转换专家。你的任务是将用户提供的文章内容精确转换为结构化的Markdown格式。请遵循以下步骤:
@ -1296,6 +1443,58 @@ onMounted(() => {
// Markdown内容变化监听 // Markdown内容变化监听
}); });
}); });
// 测试流式API
const testStreamAPI = async () => {
try {
console.log('🧪 开始测试流式API...');
const response = await fetch('http://127.0.0.1:8000/api/ai/test-stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
test: 'data'
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6));
console.log('📦 收到测试数据:', data);
} catch (e) {
console.warn('解析测试数据失败:', e);
}
}
}
}
console.log('✅ 测试流式API完成');
} catch (error) {
console.error('❌ 测试流式API失败:', error);
}
};
// 将测试函数暴露到全局,方便在控制台调用
window.testStreamAPI = testStreamAPI;
</script> </script>
<style scoped> <style scoped>

View File

@ -215,6 +215,12 @@ const hideWelcomePage = () => {
showWelcome.value = false; showWelcome.value = false;
}; };
// 显示思维导图页面(供父组件调用)
const showMindMapPage = () => {
console.log('🎯 切换到思维导图界面');
showWelcome.value = false;
};
// 加载已有思维导图 // 加载已有思维导图
const loadExistingMindmap = async () => { const loadExistingMindmap = async () => {
// 这里可以实现一个思维导图列表选择器 // 这里可以实现一个思维导图列表选择器
@ -1179,8 +1185,8 @@ const submitAIQuestion = async () => {
console.log('发送AI请求:', { systemPrompt, userPrompt }); console.log('发送AI请求:', { systemPrompt, userPrompt });
// 调用AI API // 调用流式AI API
const response = await fetch('http://127.0.0.1:8000/api/ai/generate-markdown', { const response = await fetch('http://127.0.0.1:8000/api/ai/generate-stream', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -1198,17 +1204,36 @@ const submitAIQuestion = async () => {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
const data = await response.json(); // 处理流式响应
let aiResponse = ''; let aiResponse = '';
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
if (data.success && data.markdown) { while (true) {
aiResponse = data.markdown; const { done, value } = await reader.read();
} else if (data.markdown) { if (done) break;
aiResponse = data.markdown;
} else if (data.content) { buffer += decoder.decode(value, { stream: true });
aiResponse = data.content; const lines = buffer.split('\n');
} else { buffer = lines.pop() || '';
throw new Error('AI响应格式错误');
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6));
if (data.type === 'chunk') {
aiResponse += data.content;
// 可以在这里实时更新UI显示
} else if (data.type === 'error') {
throw new Error(data.content);
}
} catch (e) {
console.warn('解析流式数据失败:', e);
}
}
}
} }
console.log('AI回答:', aiResponse); console.log('AI回答:', aiResponse);
@ -2932,6 +2957,12 @@ onMounted(async () => {
savePreviewToDatabase(event.detail.data, event.detail.title); savePreviewToDatabase(event.detail.data, event.detail.title);
}); });
// 🎯 新增:监听实时思维导图更新事件
window.addEventListener('realtime-mindmap-update', (event) => {
console.log('🔄 收到实时思维导图更新事件:', event.detail);
updateMindMapRealtime(event.detail.data, event.detail.title);
});
// 监听从历史记录加载思维导图事件 // 监听从历史记录加载思维导图事件
window.addEventListener('loadMindmapFromHistory', (event) => { window.addEventListener('loadMindmapFromHistory', (event) => {
console.log('📚 收到从历史记录加载思维导图事件:', event.detail); console.log('📚 收到从历史记录加载思维导图事件:', event.detail);
@ -3064,9 +3095,115 @@ const manuallyAddNodeToDOM = async (newNode, parentNode) => {
// 暴露方法给父组件 // 暴露方法给父组件
defineExpose({ defineExpose({
// 删除导入功能,只保留预览和保存功能 showMindMapPage,
cleanupIntervals cleanupIntervals
}); });
// 实时更新思维导图(用于流式生成)
const updateMindMapRealtime = async (data, title) => {
try {
console.log('🔄 开始实时更新思维导图:', title);
console.log('🔄 接收到的数据:', data);
console.log('🔄 数据块数量:', data.chunkCount || '未知');
// 如果还没有思维导图实例,先创建一个临时的
if (!mindElixir.value) {
console.log('🆕 创建新的思维导图实例用于实时更新');
// 隐藏欢迎页面
hideWelcomePage();
console.log('✅ 欢迎页面已隐藏');
// 等待Vue响应式更新完成
await nextTick();
console.log('✅ Vue响应式更新完成');
// 确保思维导图容器已经准备好
let retryCount = 0;
while (!mindmapEl.value && retryCount < 20) {
console.log(`⏳ 等待思维导图容器准备... (${retryCount + 1}/20)`);
await new Promise(resolve => setTimeout(resolve, 50));
await nextTick();
retryCount++;
}
if (!mindmapEl.value) {
console.error('❌ 思维导图容器仍未准备好,跳过此次更新');
return;
}
console.log('🎯 开始创建Mind Elixir实例...');
mindElixir.value = new MindElixir({
el: mindmapEl.value,
direction: MindElixir.RIGHT,
draggable: true,
contextMenu: true,
toolBar: true,
nodeMenu: false,
keypress: true,
autoCenter: false,
infinite: true,
maxScale: 5,
minScale: 0.1
});
// 🔧 修复:将数据包装成MindElixir期望的格式
const mindElixirData = {
nodeData: data, // 将节点数据放在nodeData字段中
mindmapId: `temp-${Date.now()}`, // 临时ID
title: title || 'AI生成中...'
};
// 初始化数据
const result = mindElixir.value.init(mindElixirData);
console.log('✅ 实时思维导图实例创建成功');
console.log('✅ 初始化结果:', result);
// 绑定事件监听器
bindEventListeners();
console.log('✅ 事件监听器已绑定');
// 居中显示
centerMindMap();
console.log('✅ 思维导图已居中显示');
} else {
// 如果已有实例,直接更新数据
console.log('🔄 更新现有思维导图数据');
try {
// 保存当前位置
const currentPosition = saveCurrentPosition();
console.log(' 当前位置已保存:', currentPosition);
// 🔧 修复:将数据包装成MindElixir期望的格式
const mindElixirData = {
nodeData: data, // 将节点数据放在nodeData字段中
mindmapId: currentMindmapId.value || `temp-${Date.now()}`,
title: title || 'AI生成中...'
};
// 重新初始化数据
const result = mindElixir.value.init(mindElixirData);
console.log('✅ 思维导图数据更新成功');
console.log('✅ 更新结果:', result);
// 恢复位置
if (currentPosition) {
setTimeout(() => {
restorePosition(currentPosition);
console.log(' 位置已恢复');
}, 100);
}
} catch (error) {
console.error('❌ 更新思维导图数据失败:', error);
}
}
} catch (error) {
console.error('❌ 实时更新思维导图失败:', error);
}
};
</script> </script>
<style scoped> <style scoped>