修复思维导图实时渲染问题
This commit is contained in:
parent
7b6601e010
commit
fd1b71dd75
|
|
@ -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.
|
|
@ -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 = ""
|
||||||
|
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
|
||||||
|
|
||||||
# 处理可能的JSON格式响应
|
return generate_stream()
|
||||||
try:
|
else:
|
||||||
# 尝试解析为JSON
|
# 非流式响应处理
|
||||||
json_data = json.loads(content)
|
content = response.choices[0].message.content
|
||||||
if 'answer' in json_data:
|
print(f"AI原始响应: {content}")
|
||||||
content = json_data['answer']
|
|
||||||
print(f"从JSON中提取answer: {content[:100]}...")
|
|
||||||
elif 'content' in json_data:
|
|
||||||
content = json_data['content']
|
|
||||||
print(f"从JSON中提取content: {content[:100]}...")
|
|
||||||
elif 'markdown' in json_data:
|
|
||||||
content = json_data['markdown']
|
|
||||||
print(f"从JSON中提取markdown: {content[:100]}...")
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
# 不是JSON格式,直接使用内容
|
|
||||||
print("响应不是JSON格式,直接使用内容")
|
|
||||||
|
|
||||||
# 清理内容
|
# 处理可能的JSON格式响应
|
||||||
content = content.strip()
|
try:
|
||||||
|
# 尝试解析为JSON
|
||||||
|
json_data = json.loads(content)
|
||||||
|
if 'answer' in json_data:
|
||||||
|
content = json_data['answer']
|
||||||
|
print(f"从JSON中提取answer: {content[:100]}...")
|
||||||
|
elif 'content' in json_data:
|
||||||
|
content = json_data['content']
|
||||||
|
print(f"从JSON中提取content: {content[:100]}...")
|
||||||
|
elif 'markdown' in json_data:
|
||||||
|
content = json_data['markdown']
|
||||||
|
print(f"从JSON中提取markdown: {content[:100]}...")
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
# 不是JSON格式,直接使用内容
|
||||||
|
print("响应不是JSON格式,直接使用内容")
|
||||||
|
|
||||||
# 如果返回的内容包含代码块标记,提取其中的内容
|
# 清理内容
|
||||||
markdown_match = re.search(r"```(?:markdown)?\n(.*?)```", content, re.DOTALL)
|
content = content.strip()
|
||||||
if markdown_match:
|
|
||||||
content = markdown_match.group(1).strip()
|
|
||||||
|
|
||||||
# 如果内容为空,返回模拟数据
|
# 如果返回的内容包含代码块标记,提取其中的内容
|
||||||
if not content:
|
markdown_match = re.search(r"```(?:markdown)?\n(.*?)```", content, re.DOTALL)
|
||||||
print("AI返回内容为空,使用模拟数据")
|
if markdown_match:
|
||||||
return f"""# {user_prompt}
|
content = markdown_match.group(1).strip()
|
||||||
|
|
||||||
|
# 如果内容为空,返回模拟数据
|
||||||
|
if not content:
|
||||||
|
print("AI返回内容为空,使用模拟数据")
|
||||||
|
return f"""# {user_prompt}
|
||||||
|
|
||||||
## 概述
|
## 概述
|
||||||
{user_prompt}是一个重要的概念和领域。
|
{user_prompt}是一个重要的概念和领域。
|
||||||
|
|
@ -110,7 +155,7 @@ def call_ai_api(system_prompt, user_prompt, model="glm-4.5", base_url="https://o
|
||||||
- 趋势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}")
|
||||||
|
|
|
||||||
|
|
@ -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), # 添加测试端点
|
||||||
]
|
]
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue