feat: 完善AI询问功能 - 优化UI样式和交互体验

- 修复AI节点内容显示问题,确保完整回答内容正确显示
- 优化Markdown转JSON逻辑,改进列表项处理
- 增加AI API调用重试机制,解决内容截断问题
- 实现Ask AI功能:为每个节点添加AI询问能力
- 优化AI输入框样式:扩展宽度、居中按钮、支持回车键提交
- 为Ask AI菜单项添加特殊渐变样式,提升视觉识别度
- 简化菜单项文本和悬停提示
- 修复思维导图显示和菜单同步问题
This commit is contained in:
lixinran 2025-09-08 15:06:08 +08:00
parent 5b73857835
commit 0a64d46ea3
4 changed files with 538 additions and 70 deletions

Binary file not shown.

View File

@ -50,7 +50,7 @@ def call_ai_api(system_prompt, user_prompt, model="glm-4.5", base_url="https://o
model=model, model=model,
messages=messages, messages=messages,
temperature=0.7, temperature=0.7,
max_tokens=2000, max_tokens=8000, # 增加token限制避免内容截断
stream=False stream=False
) )
except Exception as e: except Exception as e:

View File

@ -77,15 +77,15 @@
</div> </div>
</div> </div>
<!-- AI生成的Markdown结果 --> <!-- AI生成的Markdown结果 - 固定显示区域 -->
<div class="section" v-if="markdownContent"> <div class="section">
<h4>📝 AI生成的Markdown结果</h4> <h4>📝 AI生成的Markdown结果</h4>
<div class="input-group"> <div class="input-group">
<label>Markdown内容</label> <label>Markdown内容</label>
<textarea <textarea
v-model="markdownContent" v-model="markdownContent"
placeholder="AI生成的Markdown内容将显示在这里" placeholder="AI生成的Markdown内容将显示在这里"
rows="8" rows="6"
readonly readonly
class="markdown-result" class="markdown-result"
></textarea> ></textarea>
@ -100,9 +100,9 @@
</div> </div>
</div> </div>
<!-- 转换结果 --> <!-- Markdown转JSON结果 - 固定显示区域 -->
<div class="section" v-if="convertedJSON" style="display: none;"> <div class="section">
<h4>📊 转换结果</h4> <h4>📊 Markdown转JSON结果</h4>
<!-- 处理状态显示 --> <!-- 处理状态显示 -->
<div v-if="isProcessing" class="processing-status"> <div v-if="isProcessing" class="processing-status">
@ -111,7 +111,7 @@
</div> </div>
<div class="result-container"> <div class="result-container">
<pre class="json-result">{{ convertedJSON }}</pre> <pre class="json-result">{{ convertedJSON || 'JSON转换结果将显示在这里' }}</pre>
<div class="button-group"> <div class="button-group">
<button @click="copyJSON" class="btn-copy">📋 复制JSON</button> <button @click="copyJSON" class="btn-copy">📋 复制JSON</button>
<button @click="previewMindmap" :disabled="isProcessing" class="btn-copy"> <button @click="previewMindmap" :disabled="isProcessing" class="btn-copy">
@ -122,7 +122,7 @@
</div> </div>
<!-- 快速测试 --> <!-- 快速测试 -->
<div class="section" style="display: none;"> <div class="section">
<h4>🧪 快速测试</h4> <h4>🧪 快速测试</h4>
<div class="button-group"> <div class="button-group">
<button @click="loadTestData" class="btn-test">📊 加载测试数据</button> <button @click="loadTestData" class="btn-test">📊 加载测试数据</button>
@ -440,7 +440,7 @@ const generateMarkdown = async () => {
}; };
// AI APIMarkdown // AI APIMarkdown
const callAIMarkdownAPI = async (systemPrompt, userPrompt) => { const callAIMarkdownAPI = async (systemPrompt, userPrompt, retryCount = 0) => {
const defaultSystemPrompt = `你是一位Markdown格式转换专家。你的任务是将用户提供的文章内容精确转换为结构化的Markdown格式。请遵循以下步骤 const defaultSystemPrompt = `你是一位Markdown格式转换专家。你的任务是将用户提供的文章内容精确转换为结构化的Markdown格式。请遵循以下步骤
提取主标题 识别文章最顶层的主标题通常为文章题目或书名并使用Markdown的 # 级别表示 提取主标题 识别文章最顶层的主标题通常为文章题目或书名并使用Markdown的 # 级别表示
@ -515,19 +515,52 @@ Level 4 标题用 #####
const data = await response.json(); const data = await response.json();
console.log('📡 AI API响应:', data); console.log('📡 AI API响应:', data);
let markdownContent = '';
if (data.success && data.markdown) { if (data.success && data.markdown) {
console.log('✅ 从success.markdown获取内容'); console.log('✅ 从success.markdown获取内容');
return data.markdown; markdownContent = data.markdown;
} else if (data.markdown) { } else if (data.markdown) {
console.log('✅ 从markdown字段获取内容'); console.log('✅ 从markdown字段获取内容');
return data.markdown; markdownContent = data.markdown;
} else if (data.content) { } else if (data.content) {
console.log('✅ 从content字段获取内容'); console.log('✅ 从content字段获取内容');
return data.content; markdownContent = data.content;
} else { } else {
console.error('❌ AI API响应格式错误:', data); console.error('❌ AI API响应格式错误:', data);
return '生成失败,请重试'; return '生成失败,请重试';
} }
//
console.log('📝 获取到的Markdown内容长度:', markdownContent.length);
console.log('📝 内容预览:', markdownContent.substring(0, 200) + '...');
console.log('📝 内容结尾:', '...' + markdownContent.substring(markdownContent.length - 100));
//
const isTruncated = markdownContent.trim().endsWith('-') ||
markdownContent.trim().endsWith('•') ||
markdownContent.trim().endsWith('*') ||
markdownContent.trim().endsWith('**') ||
markdownContent.trim().endsWith('###') ||
markdownContent.trim().endsWith('##') ||
markdownContent.trim().endsWith('#') ||
markdownContent.trim().endsWith('时') ||
markdownContent.trim().endsWith('的') ||
markdownContent.trim().endsWith('和') ||
markdownContent.trim().endsWith('或');
if (isTruncated && retryCount < 2) {
console.warn(`⚠️ 检测到内容可能被截断,进行第${retryCount + 1}次重试`);
showNotification(`检测到内容可能被截断,正在重试...${retryCount + 1}/2`, 'warning');
// 1
await new Promise(resolve => setTimeout(resolve, 1000));
return await callAIMarkdownAPI(systemPrompt, userPrompt, retryCount + 1);
} else if (isTruncated) {
console.warn('⚠️ 内容可能被截断,但已达到最大重试次数');
showNotification('内容可能被截断,请尝试上传较小的文档或稍后重试', 'warning');
}
return markdownContent;
} catch (error) { } catch (error) {
if (error.name === 'AbortError') { if (error.name === 'AbortError') {
throw new Error('AI API请求超时2分钟文档可能较复杂请重试或尝试上传较小的文档'); throw new Error('AI API请求超时2分钟文档可能较复杂请重试或尝试上传较小的文档');
@ -606,7 +639,7 @@ const countNodes = (node) => {
return count; return count;
}; };
// MarkdownJSON // MarkdownJSON -
const markdownToJSON = (markdown) => { const markdownToJSON = (markdown) => {
const lines = markdown.split('\n'); const lines = markdown.split('\n');
let root = null; let root = null;
@ -624,10 +657,9 @@ const markdownToJSON = (markdown) => {
if (currentContent.length > 0 && stack.length > 0) { if (currentContent.length > 0 && stack.length > 0) {
const content = currentContent.join('\n').trim(); const content = currentContent.join('\n').trim();
if (content) { if (content) {
// Markdown //
const lastNode = stack[stack.length - 1]; const processedContent = processContentIntelligently(content, stack[stack.length - 1], nodeCounter);
const formattedContent = formatMarkdownToText(content); nodeCounter = processedContent.nodeCounter;
lastNode.topic = lastNode.topic + '\n\n' + formattedContent;
} }
currentContent = []; currentContent = [];
} }
@ -647,17 +679,6 @@ const markdownToJSON = (markdown) => {
data: {} data: {}
}; };
//
if (currentContent.length > 0) {
const content = currentContent.join('\n').trim();
if (content) {
// Markdown
const formattedContent = formatMarkdownToText(content);
node.topic = cleanTitle + '\n\n' + formattedContent;
}
currentContent = [];
}
// //
if (level === 1 && !root) { if (level === 1 && !root) {
root = node; root = node;
@ -678,48 +699,17 @@ const markdownToJSON = (markdown) => {
stack.push(node); stack.push(node);
} }
} else if (trimmed) { } else if (trimmed) {
// //
currentContent.push(trimmed); currentContent.push(trimmed);
} }
//
const listMatch = trimmed.match(/^[-*+]\s+(.+)$/);
if (listMatch) {
const title = listMatch[1].trim();
// Markdown
const cleanTitle = formatMarkdownToText(title);
const node = {
id: `node_${nodeCounter++}`,
topic: cleanTitle,
children: [],
level: stack.length > 0 ? stack[stack.length - 1].level + 1 : 1,
data: {}
};
if (stack.length > 0) {
stack[stack.length - 1].children.push(node);
} else if (!root) {
//
root = {
id: `node_${nodeCounter++}`,
topic: '主题',
children: [node],
level: 0,
data: {}
};
stack.push(root);
}
}
}); });
// //
if (currentContent.length > 0 && stack.length > 0) { if (currentContent.length > 0 && stack.length > 0) {
const content = currentContent.join('\n').trim(); const content = currentContent.join('\n').trim();
if (content) { if (content) {
// Markdown const processedContent = processContentIntelligently(content, stack[stack.length - 1], nodeCounter);
const lastNode = stack[stack.length - 1]; nodeCounter = processedContent.nodeCounter;
const formattedContent = formatMarkdownToText(content);
lastNode.topic = lastNode.topic + '\n\n' + formattedContent;
} }
} }
@ -736,6 +726,80 @@ const markdownToJSON = (markdown) => {
return root; return root;
}; };
//
const processContentIntelligently = (content, parentNode, nodeCounter) => {
const lines = content.split('\n');
let currentNodeCounter = nodeCounter;
let remainingContent = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const trimmed = line.trim();
//
const leafMatch = trimmed.match(/^[-*+]\s*【(.+)】/);
if (leafMatch) {
//
const leafTitle = leafMatch[1].trim();
const leafNode = {
id: `node_${currentNodeCounter++}`,
topic: leafTitle,
children: [],
level: (parentNode.level || 0) + 1,
data: {}
};
//
let leafContent = [];
let j = i + 1;
while (j < lines.length) {
const nextLine = lines[j].trim();
const nextLeafMatch = nextLine.match(/^[-*+]\s*【(.+)】/);
if (nextLeafMatch) {
//
break;
}
//
const listMatch = nextLine.match(/^[-*+]\s+(.+)$/);
if (listMatch) {
const listItem = listMatch[1].trim();
const cleanListItem = formatMarkdownToText(listItem);
leafContent.push('• ' + cleanListItem);
}
j++;
}
//
if (leafContent.length > 0) {
const formattedContent = formatMarkdownToText(leafContent.join('\n'));
leafNode.topic = leafNode.topic + '\n\n' + formattedContent;
}
parentNode.children.push(leafNode);
//
i = j - 1;
} else if (trimmed) {
//
remainingContent.push(trimmed);
}
}
//
if (remainingContent.length > 0) {
const finalContent = remainingContent.join('\n').trim();
if (finalContent) {
const formattedContent = formatMarkdownToText(finalContent);
parentNode.topic = parentNode.topic + '\n\n' + formattedContent;
}
}
return { nodeCounter: currentNodeCounter };
};
// Markdown // Markdown
const copyMarkdown = async () => { const copyMarkdown = async () => {
if (!markdownContent.value) { if (!markdownContent.value) {
@ -838,11 +902,13 @@ const previewMindmap = async () => {
processingMessage.value = ''; processingMessage.value = '';
showNotification('思维导图已保存成功!', 'success'); showNotification('思维导图已保存成功!', 'success');
// //
aiPrompt.value = ''; // ""
markdownContent.value = ''; // aiPrompt.value = '';
convertedJSON.value = ''; // markdownContent.value = '';
// convertedJSON.value = '';
//
uploadedFile.value = null; uploadedFile.value = null;
if (fileInput.value) { if (fileInput.value) {
fileInput.value.value = ''; fileInput.value.value = '';

View File

@ -70,8 +70,38 @@
</div> </div>
</div> </div>
<!-- AI输入区域 -->
<div v-if="showAIDialog" class="ai-input-area" :style="aiInputStyle">
<div class="ai-input-header">
<span class="ai-input-title">🤖 询问AI</span>
<button @click="closeAIDialog" class="ai-close-btn">×</button>
</div>
<div class="ai-input-content">
<textarea
v-model="aiQuestion"
placeholder="请输入您的问题..."
rows="2"
:disabled="isAIProcessing"
@keydown.enter.exact="handleEnterKey"
@keydown.enter.ctrl="submitAIQuestion"
@keydown.enter.meta="submitAIQuestion"
></textarea>
<div class="ai-input-actions">
<button @click="closeAIDialog" class="btn-cancel" :disabled="isAIProcessing">
取消
</button>
<button @click="submitAIQuestion" class="btn-submit" :disabled="!aiQuestion.trim() || isAIProcessing">
<span v-if="isAIProcessing">AI思考中...</span>
<span v-else>询问AI</span>
</button>
</div>
</div>
</div>
<!-- 思维导图容器 --> <!-- 思维导图容器 -->
<div v-else ref="mindmapEl" class="mindmap-el"></div> <div v-if="!showWelcome" ref="mindmapEl" class="mindmap-el"></div>
<!-- 保存和刷新按钮 --> <!-- 保存和刷新按钮 -->
<div v-if="!showWelcome" class="save-controls"> <div v-if="!showWelcome" class="save-controls">
@ -112,6 +142,17 @@
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.4503 2.99952C16.4016 2.18908 15.7289 1.54688 14.9062 1.54688H6.46875L6.37452 1.5497C5.56408 1.5984 4.92188 2.27108 4.92188 3.09375V3.54688C4.92188 3.68495 5.0338 3.79688 5.17188 3.79688H6.35938C6.49745 3.79688 6.60938 3.68495 6.60938 3.54688V3.375L6.61309 3.34276C6.62766 3.28063 6.68343 3.23438 6.75 3.23438H14.625L14.6572 3.23809C14.7261 3.23438 14.7656 3.30029 14.7656 3.375V11.25L14.7619 11.2822C14.7473 11.3444 14.6916 11.3906 14.625 11.3906H14.4531C14.3151 11.3906 14.2031 11.5026 14.2031 11.6406V12.8281C14.2031 12.9662 14.3151 13.0781 14.4531 13.0781H14.9062L15.0005 13.0753C15.8109 13.0266 16.4531 12.3539 16.4531 11.5312V3.09375L16.4503 2.99952ZM11.5312 4.92188H3.09375C2.23943 4.92188 1.54688 5.61443 1.54688 6.46875V14.9062C1.54688 15.7606 2.23943 16.4531 3.09375 16.4531H11.5312C12.3856 16.4531 13.0781 15.7606 13.0781 14.9062V6.46875C13.0781 5.61443 12.3856 4.92188 11.5312 4.92188ZM3.37032 6.615H11.2635C11.3361 6.615 11.395 6.6739 11.395 6.74655V14.6397C11.395 14.7124 11.3361 14.7712 11.2635 14.7712H3.37032C3.29767 14.7712 3.23877 14.7124 3.23877 14.6397V6.74655C3.23877 6.6739 3.29767 6.615 3.37032 6.615ZM4.5 8.5C4.5 8.27909 4.67909 8.1 4.9 8.1H9.725C9.94591 8.1 10.125 8.27909 10.125 8.5V9.5C10.125 9.72091 9.94591 9.9 9.725 9.9H4.9C4.67909 9.9 4.5 9.72091 4.5 9.5V8.5ZM4.9 11.475C4.67909 11.475 4.5 11.6541 4.5 11.875V12.875C4.5 13.0959 4.67909 13.275 4.9 13.275H9.725C9.94591 13.275 10.125 13.0959 10.125 12.875V11.875C10.125 11.6541 9.94591 11.475 9.725 11.475H4.9Z" fill="currentColor" fill-opacity="1"></path> <path fill-rule="evenodd" clip-rule="evenodd" d="M16.4503 2.99952C16.4016 2.18908 15.7289 1.54688 14.9062 1.54688H6.46875L6.37452 1.5497C5.56408 1.5984 4.92188 2.27108 4.92188 3.09375V3.54688C4.92188 3.68495 5.0338 3.79688 5.17188 3.79688H6.35938C6.49745 3.79688 6.60938 3.68495 6.60938 3.54688V3.375L6.61309 3.34276C6.62766 3.28063 6.68343 3.23438 6.75 3.23438H14.625L14.6572 3.23809C14.7261 3.23438 14.7656 3.30029 14.7656 3.375V11.25L14.7619 11.2822C14.7473 11.3444 14.6916 11.3906 14.625 11.3906H14.4531C14.3151 11.3906 14.2031 11.5026 14.2031 11.6406V12.8281C14.2031 12.9662 14.3151 13.0781 14.4531 13.0781H14.9062L15.0005 13.0753C15.8109 13.0266 16.4531 12.3539 16.4531 11.5312V3.09375L16.4503 2.99952ZM11.5312 4.92188H3.09375C2.23943 4.92188 1.54688 5.61443 1.54688 6.46875V14.9062C1.54688 15.7606 2.23943 16.4531 3.09375 16.4531H11.5312C12.3856 16.4531 13.0781 15.7606 13.0781 14.9062V6.46875C13.0781 5.61443 12.3856 4.92188 11.5312 4.92188ZM3.37032 6.615H11.2635C11.3361 6.615 11.395 6.6739 11.395 6.74655V14.6397C11.395 14.7124 11.3361 14.7712 11.2635 14.7712H3.37032C3.29767 14.7712 3.23877 14.7124 3.23877 14.6397V6.74655C3.23877 6.6739 3.29767 6.615 3.37032 6.615ZM4.5 8.5C4.5 8.27909 4.67909 8.1 4.9 8.1H9.725C9.94591 8.1 10.125 8.27909 10.125 8.5V9.5C10.125 9.72091 9.94591 9.9 9.725 9.9H4.9C4.67909 9.9 4.5 9.72091 4.5 9.5V8.5ZM4.9 11.475C4.67909 11.475 4.5 11.6541 4.5 11.875V12.875C4.5 13.0959 4.67909 13.275 4.9 13.275H9.725C9.94591 13.275 10.125 13.0959 10.125 12.875V11.875C10.125 11.6541 9.94591 11.475 9.725 11.475H4.9Z" fill="currentColor" fill-opacity="1"></path>
</svg> </svg>
</div> </div>
<!-- Ask AI 功能 -->
<div class="context-menu-item ask-ai" @click="askAIForNode" title="Ask AI">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
<path d="M13 8H7"/>
<path d="M17 12H7"/>
<circle cx="17" cy="8" r="1"/>
</svg>
</div>
<div class="context-menu-item delete" @click="deleteSelectedNode" title="Delete"> <div class="context-menu-item delete" @click="deleteSelectedNode" title="Delete">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z" fill="currentColor"/> <path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z" fill="currentColor"/>
@ -136,11 +177,18 @@ const mindmapEl = ref(null);
const mindElixir = ref(null); const mindElixir = ref(null);
const selectedNode = ref(null); const selectedNode = ref(null);
const contextMenuStyle = ref({}); const contextMenuStyle = ref({});
const aiInputStyle = ref({});
const currentMindmapId = ref(null); // ID const currentMindmapId = ref(null); // ID
const zoomLevel = ref(1); // const zoomLevel = ref(1); //
const showWelcome = ref(true); // const showWelcome = ref(true); //
const isAISidebarCollapsed = ref(false); // AI const isAISidebarCollapsed = ref(false); // AI
// Ask AI
const showAIDialog = ref(false); // AI
const currentQuestionNode = ref(null); //
const aiQuestion = ref(''); //
const isAIProcessing = ref(false); // AI
// //
const nodePositions = ref(new Map()); // const nodePositions = ref(new Map()); //
const isDragging = ref(false); const isDragging = ref(false);
@ -350,6 +398,8 @@ const loadMindmapData = async (data, keepPosition = false) => {
centerMindMap(); centerMindMap();
} }
bindEventListeners(); bindEventListeners();
//
addNodeDescriptions();
}, 300); }, 300);
} }
}, 200); }, 200);
@ -386,6 +436,8 @@ const loadMindmapData = async (data, keepPosition = false) => {
centerMindMap(); centerMindMap();
} }
bindEventListeners(); bindEventListeners();
//
addNodeDescriptions();
}, 300); }, 300);
} }
} catch (retryError) { } catch (retryError) {
@ -409,12 +461,16 @@ const loadMindmapData = async (data, keepPosition = false) => {
if (keepPosition && currentPosition) { if (keepPosition && currentPosition) {
setTimeout(() => { setTimeout(() => {
restorePosition(currentPosition); restorePosition(currentPosition);
//
addNodeDescriptions();
}, 500); }, 500);
} else { } else {
// //
setTimeout(() => { setTimeout(() => {
centerMindMap(); centerMindMap();
console.log('🔄 思维导图二次居中完成'); console.log('🔄 思维导图二次居中完成');
//
addNodeDescriptions();
}, 500); }, 500);
} }
}, 100); }, 100);
@ -444,7 +500,7 @@ const convertToMindElixirFormat = (data) => {
id: node.id, id: node.id,
topic: node.title || node.content || "无标题", topic: node.title || node.content || "无标题",
data: { data: {
des: node.des || node.description || "" des: node.des || ""
}, },
children: [], children: [],
// ID // ID
@ -1033,6 +1089,199 @@ const deleteSelectedNode = async () => {
selectedNode.value = null; selectedNode.value = null;
}; };
// Ask AI
const askAIForNode = async () => {
if (!selectedNode.value) return;
console.log('Ask AI for node:', selectedNode.value);
// AI -
const menuLeft = parseFloat(contextMenuStyle.value.left) || 0;
const menuTop = parseFloat(contextMenuStyle.value.top) || 0;
aiInputStyle.value = {
left: `${menuLeft}px`,
top: `${menuTop + 60}px`, // 60px
transform: 'translateX(-50%)'
};
// AI
showAIDialog.value = true;
currentQuestionNode.value = selectedNode.value;
// selectedNode
};
// AI
const closeAIDialog = () => {
showAIDialog.value = false;
currentQuestionNode.value = null;
aiQuestion.value = '';
isAIProcessing.value = false;
// AI
selectedNode.value = null;
};
//
const getNodeContext = (node) => {
if (!node) return '';
const context = [];
//
if (node.parent && node.parent.topic) {
context.push(`父节点: ${node.parent.topic}`);
}
//
if (node.parent && node.parent.parent && node.parent.parent.topic) {
context.push(`祖父节点: ${node.parent.parent.topic}`);
}
return context.join(' | ');
};
//
const handleEnterKey = (event) => {
// Ctrl+EnterMeta+Enter
if (event.ctrlKey || event.metaKey) {
return;
}
//
event.preventDefault();
submitAIQuestion();
};
// AI
const submitAIQuestion = async () => {
if (!aiQuestion.value.trim() || !currentQuestionNode.value || isAIProcessing.value) {
return;
}
isAIProcessing.value = true;
try {
// AI
const systemPrompt = `你是一个专业的思维导图分析助手。请根据用户的问题和提供的节点信息,给出专业、有用的回答。`;
const userPrompt = `节点信息:
当前节点${currentQuestionNode.value.topic}
上下文${getNodeContext(currentQuestionNode.value)}
用户问题${aiQuestion.value}
请给出详细的回答回答应该
1. 直接回答用户的问题
2. 提供具体的建议或改进方案
3. 保持专业和有用的语调
4. 回答长度适中便于在思维导图中展示`;
console.log('发送AI请求:', { systemPrompt, userPrompt });
// AI API
const response = await fetch('http://127.0.0.1:8000/api/ai/generate-markdown', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
system_prompt: systemPrompt,
user_prompt: userPrompt,
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 data = await response.json();
let aiResponse = '';
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响应格式错误');
}
console.log('AI回答:', aiResponse);
// AI
await createAINode(currentQuestionNode.value, aiQuestion.value, aiResponse);
//
closeAIDialog();
} catch (error) {
console.error('AI请求失败:', error);
alert('AI请求失败请稍后重试');
} finally {
isAIProcessing.value = false;
}
};
// AI
const createAINode = async (parentNode, question, answer) => {
try {
console.log('开始创建AI节点...', { parentNode, question, answer });
// 使MarkdownJSON
const formatAnswer = (text) => {
return text
.replace(/^#+\s*/gm, '') //
.replace(/\*\*(.*?)\*\*/g, '$1') //
.replace(/\*(.*?)\*/g, '$1') //
.replace(/^\s*[-*+]\s*/gm, '• ') //
.replace(/\n{3,}/g, '\n\n') //
.trim();
};
const formattedAnswer = formatAnswer(answer);
// 使API
const nodeData = {
title: `问题:${question}\n\n回答${formattedAnswer}`, // title
des: `AI追问产生的节点 - ${new Date().toLocaleString()}`, // AI
parentId: parentNode.id,
isRoot: false
};
console.log('准备创建节点数据:', nodeData);
console.log('当前思维导图ID:', currentMindmapId.value);
// ID
if (!currentMindmapId.value) {
throw new Error('没有找到当前思维导图ID无法创建节点');
}
// API
const response = await mindmapAPI.addNodes(currentMindmapId.value, [nodeData]);
console.log('API创建节点响应:', response);
if (response.data && response.data.success) {
console.log('✅ AI节点创建成功开始刷新思维导图...');
//
await refreshMindMap();
console.log('✅ AI节点创建并刷新完成');
} else {
throw new Error('API创建节点失败');
}
} catch (error) {
console.error('创建AI节点失败:', error);
alert('创建AI回答节点失败: ' + error.message);
}
};
const copyNodeText = async () => { const copyNodeText = async () => {
if (!selectedNode.value) return; if (!selectedNode.value) return;
@ -1680,7 +1929,7 @@ const createNodesRecursively = async (node, mindmapId, parentId) => {
// //
const nodeResponse = await mindmapAPI.addNodes(mindmapId, { const nodeResponse = await mindmapAPI.addNodes(mindmapId, {
title: node.topic || node.title || "无标题", title: node.topic || node.title || "无标题",
des: node.data?.des || node.description || "", des: node.data?.des || "",
parentId: parentId parentId: parentId
}); });
@ -1803,6 +2052,13 @@ const bindEventListeners = () => {
}; };
} else if (!isNodeClick) { } else if (!isNodeClick) {
selectedNode.value = null; selectedNode.value = null;
// AI
if (showAIDialog.value) {
showAIDialog.value = false;
currentQuestionNode.value = null;
aiQuestion.value = '';
isAIProcessing.value = false;
}
} }
}); });
@ -2979,6 +3235,26 @@ defineExpose({
color: #dc3545; color: #dc3545;
} }
/* Ask AI 特殊样式 - 让它一眼就突出 */
.context-menu-item.ask-ai {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: 2px solid #5a67d8;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
position: relative;
}
.context-menu-item.ask-ai:hover {
background: linear-gradient(135deg, #5a67d8 0%, #667eea 100%);
color: white;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.context-menu-item.ask-ai svg {
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.2));
}
.context-menu-item svg { .context-menu-item svg {
width: 16px; width: 16px;
height: 16px; height: 16px;
@ -3028,6 +3304,132 @@ defineExpose({
visibility: visible; visibility: visible;
} }
/* AI输入区域样式 */
.ai-input-area {
position: absolute;
width: 300px;
background: white;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
z-index: 1001;
animation: slideInDown 0.3s ease;
}
@keyframes slideInDown {
from {
opacity: 0;
transform: translateX(-50%) translateY(-10px);
}
to {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
}
.ai-input-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid #e9ecef;
background: #f8f9fa;
border-radius: 12px 12px 0 0;
}
.ai-input-title {
font-size: 16px;
font-weight: 600;
color: #2c3e50;
}
.ai-close-btn {
background: none;
border: none;
cursor: pointer;
padding: 4px 8px;
border-radius: 6px;
color: #6c757d;
font-size: 18px;
line-height: 1;
transition: all 0.2s ease;
}
.ai-close-btn:hover {
background: #e9ecef;
color: #495057;
}
.ai-input-content {
padding: 20px;
}
.ai-input-content textarea {
width: 100%;
padding: 12px;
border: 2px solid #e9ecef;
border-radius: 8px;
font-size: 14px;
line-height: 1.5;
resize: none;
transition: border-color 0.2s ease;
font-family: inherit;
margin-bottom: 16px;
box-sizing: border-box;
}
.ai-input-content textarea:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
}
.ai-input-content textarea:disabled {
background: #f8f9fa;
color: #6c757d;
cursor: not-allowed;
}
.ai-input-actions {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
}
.btn-cancel, .btn-submit {
padding: 8px 16px;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
border: none;
}
.btn-cancel {
background: #6c757d;
color: white;
}
.btn-cancel:hover:not(:disabled) {
background: #5a6268;
}
.btn-submit {
background: #007bff;
color: white;
}
.btn-submit:hover:not(:disabled) {
background: #0056b3;
transform: translateY(-1px);
}
.btn-cancel:disabled, .btn-submit:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* 响应式设计 */ /* 响应式设计 */