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

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,7 +12,33 @@ 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库未安装返回模拟数据")
return f"""# {user_prompt} 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}
## 概述 ## 概述
{user_prompt}是一个重要的概念和领域 {user_prompt}是一个重要的概念和领域
@ -40,57 +66,76 @@ 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:
content = response.choices[0].message.content # 流式响应处理
print(f"AI原始响应: {content}") def generate_stream():
full_content = ""
# 处理可能的JSON格式响应 try:
try: for chunk in response:
# 尝试解析为JSON if chunk.choices[0].delta.content is not None:
json_data = json.loads(content) content_chunk = chunk.choices[0].delta.content
if 'answer' in json_data: full_content += content_chunk
content = json_data['answer'] yield content_chunk
print(f"从JSON中提取answer: {content[:100]}...") print(f"流式响应完成,总长度: {len(full_content)}")
elif 'content' in json_data: except Exception as e:
content = json_data['content'] print(f"流式响应处理失败: {e}")
print(f"从JSON中提取content: {content[:100]}...") raise e
elif 'markdown' in json_data: return full_content
content = json_data['markdown']
print(f"从JSON中提取markdown: {content[:100]}...") return generate_stream()
except json.JSONDecodeError: else:
# 不是JSON格式直接使用内容 # 非流式响应处理
print("响应不是JSON格式直接使用内容") content = response.choices[0].message.content
print(f"AI原始响应: {content}")
# 清理内容
content = content.strip() # 处理可能的JSON格式响应
try:
# 如果返回的内容包含代码块标记,提取其中的内容 # 尝试解析为JSON
markdown_match = re.search(r"```(?:markdown)?\n(.*?)```", content, re.DOTALL) json_data = json.loads(content)
if markdown_match: if 'answer' in json_data:
content = markdown_match.group(1).strip() content = json_data['answer']
print(f"从JSON中提取answer: {content[:100]}...")
# 如果内容为空,返回模拟数据 elif 'content' in json_data:
if not content: content = json_data['content']
print("AI返回内容为空使用模拟数据") print(f"从JSON中提取content: {content[:100]}...")
return f"""# {user_prompt} elif 'markdown' in json_data:
content = json_data['markdown']
print(f"从JSON中提取markdown: {content[:100]}...")
except json.JSONDecodeError:
# 不是JSON格式直接使用内容
print("响应不是JSON格式直接使用内容")
# 清理内容
content = content.strip()
# 如果返回的内容包含代码块标记,提取其中的内容
markdown_match = re.search(r"```(?:markdown)?\n(.*?)```", content, re.DOTALL)
if markdown_match:
content = markdown_match.group(1).strip()
# 如果内容为空,返回模拟数据
if not content:
print("AI返回内容为空使用模拟数据")
return f"""# {user_prompt}
## 概述 ## 概述
{user_prompt}是一个重要的概念和领域 {user_prompt}是一个重要的概念和领域
@ -109,8 +154,8 @@ def call_ai_api(system_prompt, user_prompt, model="glm-4.5", base_url="https://o
- 趋势1 - 趋势1
- 趋势2 - 趋势2
- 趋势3""" - 趋势3"""
return content return content
except Exception as e: except Exception as e:
print(f"AI API调用异常: {e}") print(f"AI API调用异常: {e}")

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 APIMarkdown //
markdownContent.value = '';
// AI APIMarkdown
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); // 使APIAPI
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) => {
} }
}; };
// AIMarkdown // AIMarkdown
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;
markdownContent.value = ''; //
//
emit('start-realtime-generation');
try { try {
// AI API // AI API
const markdown = await callAIMarkdownAPI(null, aiPrompt.value); await callAIStreamAPI(null, aiPrompt.value);
markdownContent.value = markdown;
savedMarkdownContent.value = markdown;
console.log('📝 生成Markdown成!内容长度:', markdown.length, '字符'); 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 APIMarkdown // AI APIMarkdown
// 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>