Muyue e3debb3f71 feat: Enhanced Mode v2 with Smart Adaptive strategy, section-based editing, and comprehensive logging
## Enhanced Mode v2 - Smart Section-Based Editing (routes/ai.js)

  ### Server-Side Section Replacement Architecture
  - Add cleanMarkdownFromTitle() to normalize section titles for matching
  - Add extractHeaders() to list all document sections for debugging
  - Add replaceSection() to surgically replace only modified sections
  - AI now returns ONLY the modified section, not entire document
  - Server automatically replaces section in original document
  - Automatic header level correction if AI changes ## to ### or vice versa
  - Section boundary detection based on header hierarchy

  ### Enhanced Prompt and Response Format
  - Modified prompt to explicitly request ONLY modified section
  - New closing format: document (mandatory)
  - Added fallback regex for old format with warnings
  - Explicit rules: keep exact header level (## stays ##)
  - Clear section boundary definition in prompt
  - Examples with proper formatting guidelines

  ### Comprehensive Logging System
  - Log all API requests with method, endpoint, payload size
  - Log AI responses with length and preview
  - Log section matching and replacement operations
  - Log header level corrections
  - Log section not found errors with available sections list
  - Track modified sections across iterations

  ## AI Button Mutex and Preview Mode Controls (assets/js/app.js)

  ### AI Button Mutex (Prevent API Overload)
  - Add disableAIButtons() to disable all AI buttons during operations
  - Add enableAIButtons() to re-enable after completion or error
  - Disable all AI buttons at start of any AI operation
  - Re-enable in finally blocks to ensure cleanup even on errors
  - Re-enable on validation failures (e.g., no text selected for rephrase)
  - Re-enable when user clicks Apply/Cancel in rephrase mode

  ### Preview Mode Button Restrictions
  - Disable Preview button during Enhanced Mode operation
  - Disable all AI buttons in preview mode (rephrase, inconsistencies, duplications, advice, liberty)
  - Disable Save and Load buttons in preview mode
  - Re-enable all buttons when returning to edit mode
  - Proper cleanup with finally blocks

  ## Mermaid Auto-Fix System - Complete Removal

  ### Removed from assets/js/app.js
  - Remove mermaidFixAttempts Set from constructor
  - Remove setupMessageListener() and postMessage handler
  - Remove fixMermaidDiagramBackground() function
  - Simplify Mermaid error display to inline messages only
  - Remove hash-based tracking mechanism

  ### Removed from routes/index.js (Present Mode)
  - Remove entire auto-fix fetch and retry logic
  - Remove status div updates and fix notifications
  - Remove postMessage to parent window
  - Simplify to display styled error message only

  ### Current Behavior
  - Preview mode: Shows inline error with simple message
  - Present mode: Shows styled error box with instructions
  - No automatic fix attempts - manual correction only

  ## Additional Improvements
  - Clean markdown formatting (##, **, etc.) from section titles in UI badges
  - Proper section title matching ignoring markdown syntax
  - Enhanced error handling with detailed logging
  - Better user feedback during Enhanced Mode iterations

  This release improves Enhanced Mode reliability, prevents API overload through button mutex,
  simplifies Mermaid error handling, and adds comprehensive logging for debugging.
2025-10-15 14:22:40 +02:00

863 lines
29 KiB
JavaScript

const express = require('express');
const router = express.Router();
const https = require('https');
const { logger } = require('../utils/logger');
require('dotenv').config({ path: './config/.env' });
// Mistral AI Configuration
const MISTRAL_API_KEY = process.env.MISTRAL_API_KEY;
const MISTRAL_BASE_URL = process.env.MISTRAL_BASE_URL || 'https://api.mistral.ai/v1';
const MISTRAL_MODEL = process.env.MISTRAL_MODEL || 'mistral-large-latest';
const AI_ENABLED = process.env.AI_ENABLED === 'true';
// Verification middleware
function checkAIEnabled(req, res, next) {
if (!AI_ENABLED) {
return res.status(503).json({
success: false,
error: 'AI features are disabled'
});
}
if (!MISTRAL_API_KEY) {
return res.status(500).json({
success: false,
error: 'Mistral API key not configured'
});
}
next();
}
// Create a custom HTTPS agent with longer timeouts
const httpsAgent = new https.Agent({
keepAlive: true,
timeout: 120000, // 120 seconds
maxSockets: 5
});
// Function to clean markdown formatting from section titles
function cleanMarkdownFromTitle(title) {
if (!title) return title;
return title
.replace(/^#+\s*/g, '') // Remove leading # symbols
.replace(/\*\*/g, '') // Remove bold **
.replace(/\*/g, '') // Remove italic *
.replace(/__/g, '') // Remove bold __
.replace(/_/g, '') // Remove italic _
.replace(/`/g, '') // Remove code `
.replace(/~~/g, '') // Remove strikethrough ~~
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Remove links, keep text
.trim();
}
// Function to count headers in markdown content
function countHeaders(content) {
if (!content) return 0;
const lines = content.split('\n');
let count = 0;
for (const line of lines) {
if (line.trim().match(/^#{1,6}\s+/)) {
count++;
}
}
return count;
}
// Function to extract all headers with their levels
function extractHeaders(content) {
if (!content) return [];
const lines = content.split('\n');
const headers = [];
for (const line of lines) {
const match = line.trim().match(/^(#{1,6})\s+(.+)$/);
if (match) {
headers.push({
level: match[1].length,
title: match[2].trim()
});
}
}
return headers;
}
// Function to replace a section in the document
function replaceSection(originalContent, sectionTitle, newSectionContent) {
if (!originalContent || !sectionTitle || !newSectionContent) {
return originalContent;
}
const lines = originalContent.split('\n');
let sectionStartIndex = -1;
let sectionEndIndex = -1;
let sectionLevel = -1;
let originalHeaderPrefix = '';
// Clean the search title
const cleanSectionTitle = cleanMarkdownFromTitle(sectionTitle);
// Find the section header - be more tolerant on matching
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
const match = line.match(/^(#{1,6})\s+(.+)$/);
if (match) {
const headerLevel = match[1].length;
const headerTitle = cleanMarkdownFromTitle(match[2].trim());
if (headerTitle === cleanSectionTitle) {
sectionStartIndex = i;
sectionLevel = headerLevel;
originalHeaderPrefix = match[1]; // Store original # count
break;
}
}
}
if (sectionStartIndex === -1) {
// Section not found - return original content
logger.error('AI', `Section "${sectionTitle}" not found in document`);
logger.info('AI', `Available sections: ${extractHeaders(originalContent).map(h => cleanMarkdownFromTitle(h.title)).join(', ')}`);
return originalContent;
}
// Fix the header level in the new content if AI changed it
const newLines = newSectionContent.split('\n');
if (newLines.length > 0) {
const firstLineMatch = newLines[0].trim().match(/^(#{1,6})\s+(.+)$/);
if (firstLineMatch) {
const aiHeaderLevel = firstLineMatch[1].length;
if (aiHeaderLevel !== sectionLevel) {
// AI changed the header level - fix it!
logger.warn('AI', `AI changed header level from ${sectionLevel} to ${aiHeaderLevel} - fixing it`);
newLines[0] = newLines[0].replace(/^#{1,6}/, originalHeaderPrefix);
}
}
}
newSectionContent = newLines.join('\n');
// Find where the section ends (next header of equal or higher level)
for (let i = sectionStartIndex + 1; i < lines.length; i++) {
const line = lines[i].trim();
const match = line.match(/^(#{1,6})\s+/);
if (match) {
const headerLevel = match[1].length;
if (headerLevel <= sectionLevel) {
// Found next section at same or higher level
sectionEndIndex = i;
break;
}
}
}
// If no end found, section goes to end of document
if (sectionEndIndex === -1) {
sectionEndIndex = lines.length;
}
// Replace the section
const before = lines.slice(0, sectionStartIndex);
const after = lines.slice(sectionEndIndex);
const newSection = newSectionContent.split('\n');
const result = [...before, ...newSection, ...after].join('\n');
logger.info('AI', `Replaced section "${sectionTitle}" (lines ${sectionStartIndex}-${sectionEndIndex})`);
return result;
}
// Function to call Mistral API with timeout handling
async function callMistralAPI(messages, temperature = null) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 120000); // 120 seconds timeout
try {
const response = await fetch(`${MISTRAL_BASE_URL}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${MISTRAL_API_KEY}`
},
body: JSON.stringify({
model: MISTRAL_MODEL,
messages: messages,
temperature: temperature !== null ? temperature : parseFloat(process.env.AI_TEMPERATURE) || 0.3,
max_tokens: parseInt(process.env.AI_MAX_TOKENS) || 35000,
top_p: parseFloat(process.env.AI_TOP_P) || 0.85
}),
signal: controller.signal,
agent: httpsAgent
});
clearTimeout(timeoutId);
if (!response.ok) {
const error = await response.text();
throw new Error(`Mistral API Error: ${response.status} - ${error}`);
}
const data = await response.json();
return data.choices[0].message.content;
} catch (error) {
clearTimeout(timeoutId);
console.error('Mistral API Error:', error);
// Better error messages
if (error.name === 'AbortError') {
throw new Error('Request timeout: The API took too long to respond (>120s)');
}
if (error.cause && error.cause.code === 'UND_ERR_HEADERS_TIMEOUT') {
throw new Error('Connection timeout: Unable to connect to Mistral API. Check your network or API endpoint.');
}
if (error.cause && error.cause.code === 'ECONNREFUSED') {
throw new Error('Connection refused: Cannot reach Mistral API endpoint.');
}
throw error;
}
}
// POST /api/ai/rephrase - Rephrase text
router.post('/rephrase', checkAIEnabled, async (req, res) => {
const startTime = Date.now();
try {
const { text, context = '' } = req.body;
logger.aiRequest('rephrase', MISTRAL_MODEL, text.length);
if (!text || text.trim().length === 0) {
logger.aiError('rephrase', new Error('Text to rephrase is required'));
return res.status(400).json({
success: false,
error: 'Text to rephrase is required'
});
}
const messages = [
{
role: 'system',
content: `You are an assistant specialized in rephrasing technical and design texts.
STRICT RULES:
1. Rephrase the text to improve clarity, style, and fluency
2. Preserve the technical level and all important details
3. Respond ONLY with the final rephrased text
4. No introduction, conclusion, explanation, or commentary
5. Do not start with "Here is", "The rephrased text" or other preamble
6. Start directly with the rephrased content
${context ? `Document context: ${context}` : ''}`
},
{
role: 'user',
content: `Rephrase this text: "${text}"`
}
];
const result = await callMistralAPI(messages, 0.2); // Precise rephrasing
logger.aiResponse('rephrase', true, Date.now() - startTime);
res.json({
success: true,
data: {
original: text,
rephrased: result
}
});
} catch (error) {
logger.aiError('rephrase', error);
res.status(500).json({
success: false,
error: 'Error during rephrasing: ' + error.message
});
}
});
// POST /api/ai/check-inconsistencies - Check for inconsistencies
router.post('/check-inconsistencies', checkAIEnabled, async (req, res) => {
const startTime = Date.now();
try {
const { content } = req.body;
logger.aiRequest('check-inconsistencies', MISTRAL_MODEL, content.length);
if (!content || content.trim().length === 0) {
logger.aiError('check-inconsistencies', new Error('Content to analyze is required'));
return res.status(400).json({
success: false,
error: 'Content to analyze is required'
});
}
const messages = [
{
role: 'system',
content: `You are an expert in technical design document analysis.
Analyze the following document and identify all potential inconsistencies:
- Contradictions in information
- Conflicting decisions
- Inconsistencies in terminology
- Logical issues in architecture or choices
Respond directly with your detailed analysis of the inconsistencies found.`
},
{
role: 'user',
content: `Analyze this document to detect inconsistencies:\n\n${content}`
}
];
const result = await callMistralAPI(messages, 0.1); // Precise and factual analysis
logger.aiResponse('check-inconsistencies', true, Date.now() - startTime);
res.json({
success: true,
data: {
analysis: result
}
});
} catch (error) {
logger.aiError('check-inconsistencies', error);
res.status(500).json({
success: false,
error: 'Error during analysis: ' + error.message
});
}
});
// POST /api/ai/check-duplications - Check for duplications
router.post('/check-duplications', checkAIEnabled, async (req, res) => {
const startTime = Date.now();
try {
const { content } = req.body;
logger.aiRequest('check-duplications', MISTRAL_MODEL, content.length);
if (!content || content.trim().length === 0) {
logger.aiError('check-duplications', new Error('Content to analyze is required'));
return res.status(400).json({
success: false,
error: 'Content to analyze is required'
});
}
const messages = [
{
role: 'system',
content: `You are a content analysis expert.
Analyze the following document to identify:
- Repeated or redundant information
- Sections that cover the same topic
- Duplicated explanations
- Concepts presented multiple times
Suggest ways to eliminate these duplications while preserving important information.
Respond directly with your analysis and suggestions.`
},
{
role: 'user',
content: `Analyze this document to detect duplications:\n\n${content}`
}
];
const result = await callMistralAPI(messages, 0.1); // Precise and factual analysis
logger.aiResponse('check-duplications', true, Date.now() - startTime);
res.json({
success: true,
data: {
analysis: result
}
});
} catch (error) {
logger.aiError('check-duplications', error);
res.status(500).json({
success: false,
error: 'Error during analysis: ' + error.message
});
}
});
// POST /api/ai/give-advice - Give advice
router.post('/give-advice', checkAIEnabled, async (req, res) => {
const startTime = Date.now();
try {
const { content, domain = 'general' } = req.body;
logger.aiRequest('give-advice', MISTRAL_MODEL, content.length);
if (!content || content.trim().length === 0) {
logger.aiError('give-advice', new Error('Content to analyze is required'));
return res.status(400).json({
success: false,
error: 'Content to analyze is required'
});
}
const messages = [
{
role: 'system',
content: `You are an expert consultant in design and technical architecture in the domain: ${domain}.
Analyze the provided design document and give constructive advice to improve it.
Focus on:
- Documentation completeness
- Clarity of explanations
- Content organization
- Domain best practices
- Important missing points
- Concrete improvement suggestions
Be constructive and practical in your recommendations.
Respond directly with your advice and improvement suggestions.`
},
{
role: 'user',
content: `Analyze this design document and provide advice to improve it:\n\n${content}`
}
];
const result = await callMistralAPI(messages, 0.4); // Balanced advice
logger.aiResponse('give-advice', true, Date.now() - startTime);
res.json({
success: true,
data: {
advice: result
}
});
} catch (error) {
logger.aiError('give-advice', error);
res.status(500).json({
success: false,
error: 'Error during analysis: ' + error.message
});
}
});
// POST /api/ai/liberty-mode - Enhanced mode (iterative generation)
router.post('/liberty-mode', checkAIEnabled, async (req, res) => {
const startTime = Date.now();
try {
const { content, iterations = 1, precision = 70, focus = 'design' } = req.body;
logger.aiRequest('liberty-mode', MISTRAL_MODEL, content.length);
if (!content || content.trim().length === 0) {
logger.aiError('liberty-mode', new Error('Base content is required'));
return res.status(400).json({
success: false,
error: 'Base content is required'
});
}
const maxIterations = Math.min(parseInt(iterations), 10); // Limit to 10 iterations
const precisionPercent = Math.min(Math.max(parseInt(precision), 10), 100); // Between 10% and 100%
logger.info('AI', `Liberty mode started: ${maxIterations} iterations at ${precisionPercent}% precision`);
// Configure streaming for real-time responses
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*'
});
let currentContent = content;
let modifiedSections = []; // Track sections that have been modified
for (let i = 0; i < maxIterations; i++) {
try {
const messages = [
{
role: 'system',
content: `You are a technical design expert with "Enhanced Mode - Smart Adaptive Strategy".
MISSION: Analyze the entire document and choose ONE section to improve in this iteration.
SMART SECTION SELECTION CRITERIA:
1. **Quality Assessment**: Identify sections that need improvement (incomplete, vague, lacking details)
2. **Strategic Importance**: Prioritize critical sections (Introduction, Architecture, Core APIs)
3. **Coverage**: Avoid sections recently modified: ${modifiedSections.length > 0 ? modifiedSections.join(', ') : 'None yet'}
4. **Balance**: Ensure progressive document enhancement
PRECISION RULES:
- At ${precisionPercent}%: You can deduce and add content up to ${precisionPercent}% based on existing information
- At ${100 - precisionPercent}%: You can create logical and relevant content even without explicit info in the text
SECTION BOUNDARIES - CRITICAL UNDERSTANDING:
A section is defined by its header and extends until:
- The next header of EQUAL or HIGHER level appears
- The end of the document is reached
Example structure (using headers with hash symbols):
Level 1 header: Main Title
content...
Level 2 header: Section A ← Section A starts here
content A...
Level 3 header: Subsection A1 ← Part of Section A
content A1...
Level 3 header: Subsection A2 ← Part of Section A
content A2...
Level 2 header: Section B ← Section A ends, Section B starts
content B...
If you choose to modify "### Subsection A1":
- Include ONLY the content from "### Subsection A1" until "### Subsection A2"
- DO NOT include "### Subsection A2" or anything after it
- DO NOT include "## Section B" or anything after it
- The rest of the document (before and after) must remain EXACTLY as it was
INSTRUCTIONS:
1. **ANALYZE** all sections in the document
2. **CHOOSE** exactly ONE section to improve (based on criteria above)
3. **IDENTIFY BOUNDARIES**: Determine where the chosen section starts and ends:
- Starts: At the section's header (e.g., "### API Documentation")
- Ends: Right BEFORE the next header of equal or higher level, OR at document end
4. **ENHANCE** only that chosen section with:
- Additional technical details
- Better explanations
- Code examples if relevant
- Mermaid diagrams if relevant (using \`\`\`mermaid blocks)
- Best practices
5. **PRESERVE** everything else:
- ALL content BEFORE the chosen section must remain unchanged
- ALL content AFTER the chosen section must remain unchanged
- ALL other sections at any level must remain unchanged
6. **MAINTAIN** document structure and formatting
7. **CRITICAL**: Keep the EXACT same header level for the modified section
- If the original section uses ## (level 2), the modified section MUST use ##
- If the original section uses ### (level 3), the modified section MUST use ###
- DO NOT change header levels (## to ###, or ### to ##, etc.)
8. **VERIFY**: Before returning, check that the document contains ALL original sections
MANDATORY RESPONSE FORMAT - Use code blocks with triple backticks:
First, write your analysis and decision inside a code block labeled "comment${i + 1}":
- **Analyzed Sections**: List all major sections found
- **Selected Section**: Which section you chose to improve (exact header name)
- **Selection Reason**: Why this section needs improvement most
- **Improvements Made**: Detailed list of enhancements
- **Next Recommendations**: Sections to consider for future iterations
Then, write ONLY THE MODIFIED SECTION inside a code block labeled "document".
DO NOT include the entire document - ONLY the section you improved.
Example format:
\`\`\`comment${i + 1}
**Analyzed Sections**: Introduction, Architecture, API Documentation, Conclusion
**Selected Section**: ### API Documentation
**Selection Reason**: This section lacks implementation details and error handling
**Improvements Made**:
- Added authentication flow
- Included error handling scenarios
- Added code examples
**Next Recommendations**: Consider enhancing Architecture section
\`\`\`
\`\`\`document
### API Documentation
[Only the improved API Documentation section content here]
\`\`\`document
CRITICAL FORMATTING RULES - MANDATORY:
1. The document block MUST start with: \`\`\`document (three backticks + word "document")
2. The document block MUST end with: \`\`\`document (three backticks + word "document")
3. This closing tag is ESSENTIAL - without it, the content will be truncated
4. Return ONLY the modified section (from its header to the end of its content)
5. Include the section header (e.g., ### API Documentation)
6. **CRITICAL**: Keep EXACTLY the same header level (same number of #)
- If original is ### (3 hashes), your response MUST start with ###
- If original is ## (2 hashes), your response MUST start with ##
- DO NOT CHANGE: ### to ##### or ## to ###, etc.
7. Include ALL content of that section (including subsections if any)
8. DO NOT include any content from other sections
9. The server will automatically replace this section in the full document
EXAMPLE OF CORRECT FORMAT:
\`\`\`document
## My Section Title
Content here...
More content...
### Subsection if needed
Subsection content...
\`\`\`document
Focus: ${focus}
Precision: ${precisionPercent}%
Iteration: ${i + 1}/${maxIterations}
Previously Modified: ${modifiedSections.length > 0 ? modifiedSections.join(', ') : 'None'}`
},
{
role: 'user',
content: `Document to analyze and improve (Iteration ${i + 1}):\n\n${currentContent}`
}
];
// Temperature based on precision (more creative = higher temperature)
const temperature = (100 - precisionPercent) / 100 * 0.8 + 0.1; // Between 0.1 and 0.9
const result = await callMistralAPI(messages, temperature);
// Log raw AI response for debugging
logger.info('AI', `Raw AI response length: ${result.length} characters`);
logger.info('AI', `First 500 chars: ${result.substring(0, 500)}`);
// Extract code blocks
const commentRegex = /```comment\d+\s*([\s\S]*?)```/;
const documentRegex = /```document\s*([\s\S]*?)```document/;
const commentMatch = result.match(commentRegex);
let documentMatch = result.match(documentRegex);
// Log what was matched
if (documentMatch) {
logger.info('AI', `Document block matched. Length: ${documentMatch[1].length} characters`);
logger.info('AI', `Document content preview: ${documentMatch[1].substring(0, 300)}...`);
} else {
logger.warn('AI', 'No document block found with new format, trying old format');
}
// Fallback: if new format not found, try old format
if (!documentMatch) {
// Try to find the last occurrence of ```document and take everything after it until the next ```
const oldDocumentRegex = /```document\s*([\s\S]*?)(?:```|$)/;
documentMatch = result.match(oldDocumentRegex);
if (documentMatch) {
logger.info('AI', `Old format matched. Length: ${documentMatch[1].length} characters`);
logger.warn('AI', 'AI did not use the correct closing format ```document - using fallback');
}
}
let explanation = '';
let newMarkdown = currentContent; // Default, keep old content
let selectedSection = '';
if (commentMatch && commentMatch[1]) {
explanation = commentMatch[1].trim();
// Extract selected section name from comment
const sectionMatch = explanation.match(/\*\*Selected Section\*\*:?\s*(.+?)(?:\n|\*\*|$)/i);
if (sectionMatch && sectionMatch[1]) {
selectedSection = cleanMarkdownFromTitle(sectionMatch[1].trim());
// Add to modified sections tracker
if (!modifiedSections.includes(selectedSection)) {
modifiedSections.push(selectedSection);
}
}
}
if (documentMatch && documentMatch[1]) {
const modifiedSection = documentMatch[1].trim();
// Replace only the modified section in the original document
if (selectedSection) {
newMarkdown = replaceSection(currentContent, selectedSection, modifiedSection);
if (newMarkdown === currentContent) {
// Section replacement failed
logger.error('AI', `Failed to replace section "${selectedSection}" in document`);
explanation += '\n\n[ERROR: Could not find section to replace in document. Content was not updated.]';
} else {
// Section successfully replaced - update for next iteration
logger.info('AI', `Successfully replaced section "${selectedSection}"`);
currentContent = newMarkdown;
}
} else {
// No section identified - keep original
logger.error('AI', 'No section identified for replacement');
newMarkdown = currentContent;
explanation += '\n\n[ERROR: No section identified. Content was not updated.]';
}
}
// Fallback: if no code blocks found, try old format for compatibility
if (!commentMatch && !documentMatch) {
const parts = result.split('---SPLIT---');
if (parts.length >= 2) {
explanation = parts[0].trim();
newMarkdown = parts[1].trim();
currentContent = newMarkdown;
} else {
explanation = result;
}
}
// Send this iteration's response
const iterationData = {
iteration: i + 1,
explanation: explanation,
selectedSection: selectedSection || 'Unknown',
markdown: newMarkdown,
completed: false
};
res.write(`data: ${JSON.stringify(iterationData)}\n\n`);
// Small delay to allow client-side display
await new Promise(resolve => setTimeout(resolve, 500));
} catch (iterationError) {
console.error(`Iteration ${i + 1} error:`, iterationError);
const errorData = {
iteration: i + 1,
error: `Iteration ${i + 1} error: ${iterationError.message}`,
completed: true
};
res.write(`data: ${JSON.stringify(errorData)}\n\n`);
break;
}
}
// End signal
const finalData = {
completed: true,
totalIterations: maxIterations,
finalMarkdown: currentContent
};
res.write(`data: ${JSON.stringify(finalData)}\n\n`);
res.end();
logger.aiResponse('liberty-mode', true, Date.now() - startTime);
} catch (error) {
logger.aiError('liberty-mode', error);
const errorData = {
error: 'Error during generation: ' + error.message,
completed: true
};
res.write(`data: ${JSON.stringify(errorData)}\n\n`);
res.end();
}
});
// POST /api/ai/fix-mermaid - Fix Mermaid diagram errors
router.post('/fix-mermaid', checkAIEnabled, async (req, res) => {
const startTime = Date.now();
try {
const { mermaidCode, error } = req.body;
logger.aiRequest('fix-mermaid', MISTRAL_MODEL, mermaidCode ? mermaidCode.length : 0);
if (!mermaidCode || mermaidCode.trim().length === 0) {
logger.aiError('fix-mermaid', new Error('Mermaid code is required'));
return res.status(400).json({
success: false,
error: 'Mermaid code is required'
});
}
if (!error || error.trim().length === 0) {
logger.aiError('fix-mermaid', new Error('Error message is required'));
return res.status(400).json({
success: false,
error: 'Error message is required'
});
}
const messages = [
{
role: 'system',
content: `You are a Mermaid diagram syntax expert.
MISSION: Fix the Mermaid diagram syntax error.
STRICT RULES:
1. Analyze the error message carefully
2. Identify the syntax issue in the Mermaid code
3. Fix ONLY the syntax error while preserving the diagram's intent and content
4. Return ONLY the corrected Mermaid code
5. Do NOT add explanations, comments, or any text outside the code block
6. Do NOT wrap in \`\`\`mermaid - respond with raw Mermaid code only
Common Mermaid syntax issues to watch for:
- Invalid character sequences in node labels
- Missing quotes around special characters
- Incorrect bracket/parenthesis syntax for node shapes
- Invalid keywords or operators
- Line break issues (\n should be used carefully)
- Special characters that need escaping: (, ), [, ], {, }, <, >, ", '`
},
{
role: 'user',
content: `Fix this Mermaid diagram that has a syntax error.
ERROR:
${error}
MERMAID CODE:
${mermaidCode}
Return ONLY the corrected Mermaid code without any explanation or markdown formatting.`
}
];
const result = await callMistralAPI(messages, 0.1); // Very precise
logger.aiResponse('fix-mermaid', true, Date.now() - startTime);
res.json({
success: true,
data: {
originalCode: mermaidCode,
fixedCode: result.trim(),
error: error
}
});
} catch (error) {
logger.aiError('fix-mermaid', error);
res.status(500).json({
success: false,
error: 'Error fixing Mermaid diagram: ' + error.message
});
}
});
// GET /api/ai/status - AI status
router.get('/status', (req, res) => {
res.json({
success: true,
data: {
enabled: AI_ENABLED,
model: MISTRAL_MODEL,
configured: !!MISTRAL_API_KEY
}
});
});
module.exports = router;