修复思维导图实时渲染问题
This commit is contained in:
parent
7b6601e010
commit
fd1b71dd75
|
|
@ -103,6 +103,44 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
|||
# CORS settings
|
||||
CORS_ALLOW_ALL_ORIGINS = 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 = {
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -2,7 +2,7 @@ import re
|
|||
import traceback
|
||||
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
|
||||
"""
|
||||
|
|
@ -12,6 +12,32 @@ def call_ai_api(system_prompt, user_prompt, model="glm-4.5", base_url="https://o
|
|||
from openai import OpenAI
|
||||
except ImportError:
|
||||
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}
|
||||
|
||||
## 概述
|
||||
|
|
@ -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"模型: {model}")
|
||||
print(f"流式模式: {stream}")
|
||||
|
||||
# 创建OpenAI客户端
|
||||
client = OpenAI(api_key=api_key, base_url=base_url)
|
||||
|
||||
# 使用非流式调用,更简单可靠
|
||||
# 根据stream参数决定是否使用流式调用
|
||||
try:
|
||||
response = client.chat.completions.create(
|
||||
model=model,
|
||||
messages=messages,
|
||||
temperature=0.7,
|
||||
max_tokens=8000, # 增加token限制,避免内容截断
|
||||
stream=False
|
||||
stream=stream
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"API调用失败: {e}")
|
||||
# 如果API调用失败,抛出异常而不是返回模拟数据
|
||||
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
|
||||
print(f"AI原始响应: {content}")
|
||||
|
||||
|
|
|
|||
|
|
@ -11,4 +11,6 @@ urlpatterns = [
|
|||
|
||||
# AI接口
|
||||
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 django.db import transaction
|
||||
from django.utils import timezone
|
||||
from django.http import StreamingHttpResponse
|
||||
import json
|
||||
|
||||
from .models import mindMap, Node
|
||||
from .serializers import map_mindmap_to_doc, map_node_to_doc
|
||||
|
|
@ -468,3 +470,141 @@ def generate_markdown(request):
|
|||
}, 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>
|
||||
<div id="app">
|
||||
<!-- AI侧边栏 -->
|
||||
<AISidebar />
|
||||
<AISidebar @start-realtime-generation="handleStartRealtimeGeneration" />
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<div class="main-content">
|
||||
|
|
@ -11,8 +11,19 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import MindMap from "./components/MindMap.vue";
|
||||
import AISidebar from "./components/AISidebar.vue";
|
||||
|
||||
const mindMapRef = ref(null);
|
||||
|
||||
// 处理开始实时生成事件
|
||||
const handleStartRealtimeGeneration = () => {
|
||||
console.log('🎬 开始实时生成,切换到思维导图界面');
|
||||
if (mindMapRef.value) {
|
||||
mindMapRef.value.showMindMapPage();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
|
|
|||
|
|
@ -171,6 +171,9 @@ import { ref, reactive } from 'vue';
|
|||
import axios from 'axios';
|
||||
import { marked } from 'marked';
|
||||
|
||||
// 定义emit事件
|
||||
const emit = defineEmits(['start-realtime-generation']);
|
||||
|
||||
// 响应式数据
|
||||
const isCollapsed = ref(false); // 默认展开状态
|
||||
const aiPrompt = ref('');
|
||||
|
|
@ -277,26 +280,35 @@ const generateMarkdownFromFile = async () => {
|
|||
|
||||
isGenerating.value = true;
|
||||
|
||||
// 立即切换到思维导图界面,开始实时渲染
|
||||
emit('start-realtime-generation');
|
||||
|
||||
try {
|
||||
// 读取文件内容
|
||||
const fileContent = await readFileContent(uploadedFile.value);
|
||||
console.log('📄 文件内容预览:', fileContent.substring(0, 200) + '...');
|
||||
|
||||
// 调用AI API生成Markdown
|
||||
// 清空之前的内容,准备流式生成
|
||||
markdownContent.value = '';
|
||||
|
||||
// 调用流式AI API生成Markdown
|
||||
const systemPrompt = '你是一个专业的文档分析专家。请分析上传的文档内容,生成结构化的Markdown格式思维导图。要求:1. 提取主要主题和关键概念 2. 组织成层次分明的结构 3. 使用清晰的标题和子标题 4. 保持内容的逻辑性和完整性';
|
||||
const userPrompt = `请分析以下文档内容并生成结构化Markdown:\n\n${fileContent}`;
|
||||
|
||||
console.log('🤖 系统提示词:', systemPrompt);
|
||||
console.log('👤 用户提示词预览:', userPrompt.substring(0, 200) + '...');
|
||||
|
||||
const markdown = await callAIMarkdownAPI(systemPrompt, userPrompt);
|
||||
// 关键修改:使用流式API而不是非流式API
|
||||
await callAIStreamAPI(systemPrompt, userPrompt);
|
||||
|
||||
markdownContent.value = markdown;
|
||||
console.log('📝 生成Markdown成功!内容长度:', markdown.length, '字符');
|
||||
console.log('📝 流式生成Markdown完成!内容长度:', markdownContent.value.length, '字符');
|
||||
|
||||
// 自动转换为JSON
|
||||
await convertToJSON();
|
||||
|
||||
// 添加到历史记录
|
||||
addToHistory('AI生成: ' + uploadedFile.value.name, markdownContent.value);
|
||||
|
||||
showNotification('AI生成Markdown成功!正在自动保存...', 'success');
|
||||
|
||||
// 延迟一下让用户看到成功消息,然后自动保存
|
||||
|
|
@ -405,7 +417,7 @@ const extractPDFContent = async (file) => {
|
|||
}
|
||||
};
|
||||
|
||||
// AI生成Markdown
|
||||
// AI生成Markdown(流式版本)
|
||||
const generateMarkdown = async () => {
|
||||
if (!aiPrompt.value.trim()) {
|
||||
showNotification('请输入主题描述', 'error');
|
||||
|
|
@ -416,19 +428,22 @@ const generateMarkdown = async () => {
|
|||
savedAiPrompt.value = aiPrompt.value;
|
||||
|
||||
isGenerating.value = true;
|
||||
try {
|
||||
// 调用真实的AI API
|
||||
const markdown = await callAIMarkdownAPI(null, aiPrompt.value);
|
||||
markdownContent.value = markdown;
|
||||
savedMarkdownContent.value = markdown;
|
||||
markdownContent.value = ''; // 清空之前的内容
|
||||
|
||||
console.log('📝 生成Markdown成功!内容长度:', markdown.length, '字符');
|
||||
// 立即切换到思维导图界面,开始实时渲染
|
||||
emit('start-realtime-generation');
|
||||
|
||||
try {
|
||||
// 调用流式AI API
|
||||
await callAIStreamAPI(null, aiPrompt.value);
|
||||
|
||||
console.log('📝 流式生成Markdown完成!内容长度:', markdownContent.value.length, '字符');
|
||||
|
||||
// 自动转换为JSON
|
||||
await convertToJSON();
|
||||
|
||||
// 添加到历史记录
|
||||
addToHistory('AI生成: ' + aiPrompt.value.substring(0, 30) + '...', markdown);
|
||||
addToHistory('AI生成: ' + aiPrompt.value.substring(0, 30) + '...', markdownContent.value);
|
||||
|
||||
// 确保AI提示词不被清空
|
||||
if (aiPrompt.value !== savedAiPrompt.value) {
|
||||
|
|
@ -456,6 +471,138 @@ const generateMarkdown = async () => {
|
|||
};
|
||||
|
||||
// 调用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 defaultSystemPrompt = `你是一位Markdown格式转换专家。你的任务是将用户提供的文章内容精确转换为结构化的Markdown格式。请遵循以下步骤:
|
||||
|
||||
|
|
@ -1296,6 +1443,58 @@ onMounted(() => {
|
|||
// 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>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
|||
|
|
@ -215,6 +215,12 @@ const hideWelcomePage = () => {
|
|||
showWelcome.value = false;
|
||||
};
|
||||
|
||||
// 显示思维导图页面(供父组件调用)
|
||||
const showMindMapPage = () => {
|
||||
console.log('🎯 切换到思维导图界面');
|
||||
showWelcome.value = false;
|
||||
};
|
||||
|
||||
// 加载已有思维导图
|
||||
const loadExistingMindmap = async () => {
|
||||
// 这里可以实现一个思维导图列表选择器
|
||||
|
|
@ -1179,8 +1185,8 @@ const submitAIQuestion = async () => {
|
|||
|
||||
console.log('发送AI请求:', { systemPrompt, userPrompt });
|
||||
|
||||
// 调用AI API
|
||||
const response = await fetch('http://127.0.0.1:8000/api/ai/generate-markdown', {
|
||||
// 调用流式AI API
|
||||
const response = await fetch('http://127.0.0.1:8000/api/ai/generate-stream', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
|
@ -1198,17 +1204,36 @@ const submitAIQuestion = async () => {
|
|||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
// 处理流式响应
|
||||
let aiResponse = '';
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
|
||||
if (data.success && data.markdown) {
|
||||
aiResponse = data.markdown;
|
||||
} else if (data.markdown) {
|
||||
aiResponse = data.markdown;
|
||||
} else if (data.content) {
|
||||
aiResponse = data.content;
|
||||
} else {
|
||||
throw new Error('AI响应格式错误');
|
||||
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 === 'chunk') {
|
||||
aiResponse += data.content;
|
||||
// 可以在这里实时更新UI显示
|
||||
} else if (data.type === 'error') {
|
||||
throw new Error(data.content);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('解析流式数据失败:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('AI回答:', aiResponse);
|
||||
|
|
@ -2932,6 +2957,12 @@ onMounted(async () => {
|
|||
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) => {
|
||||
console.log('📚 收到从历史记录加载思维导图事件:', event.detail);
|
||||
|
|
@ -3064,9 +3095,115 @@ const manuallyAddNodeToDOM = async (newNode, parentNode) => {
|
|||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
// 删除导入功能,只保留预览和保存功能
|
||||
showMindMapPage,
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
|||
Loading…
Reference in New Issue