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.
This commit is contained in:
Augustin ROUX 2025-10-15 14:22:40 +02:00
parent 86eb68c0e6
commit e3debb3f71
3 changed files with 325 additions and 140 deletions

View File

@ -14,30 +14,11 @@ class ConceptionAssistant {
async init() {
this.setupEditor();
this.setupEventListeners();
this.setupMessageListener();
await this.loadJournalList();
this.generateTOC();
this.updateStatistics();
}
setupMessageListener() {
// Listen for messages from presentation mode window
window.addEventListener('message', (event) => {
if (event.data && event.data.type === 'mermaid-fixed') {
// Update the document content with the fixed Mermaid diagram
const currentContent = this.editor.innerText;
const fixedContent = currentContent.replace(event.data.originalCode, event.data.fixedCode);
if (fixedContent !== currentContent) {
this.editor.innerText = fixedContent;
this.generateTOC();
this.saveState(true);
this.showNotification('Mermaid diagram fixed in document', 'success');
}
}
});
}
setupEditor() {
this.editor = document.getElementById('journal-editor');
@ -928,9 +909,13 @@ class ConceptionAssistant {
const selection = window.getSelection().toString().trim();
const fullContent = this.editor.innerText;
// Disable all AI buttons to prevent API overload
this.disableAIButtons();
// Check conditions based on action
if (action === 'rephrase' && !selection) {
this.showAIFeedback('Please select text to rephrase');
this.enableAIButtons();
return;
}
@ -993,8 +978,14 @@ class ConceptionAssistant {
`);
// Add event listeners for buttons
document.getElementById('validate-rephrase')?.addEventListener('click', () => this.validateRephrase());
document.getElementById('cancel-rephrase')?.addEventListener('click', () => this.clearFeedback());
document.getElementById('validate-rephrase')?.addEventListener('click', () => {
this.validateRephrase();
this.enableAIButtons();
});
document.getElementById('cancel-rephrase')?.addEventListener('click', () => {
this.clearFeedback();
this.enableAIButtons();
});
break;
case 'inconsistencies':
@ -1016,6 +1007,14 @@ class ConceptionAssistant {
// Save state before AI modifications
this.saveState(true);
// Disable preview button during enhanced mode
const previewBtn = document.getElementById('preview-toggle');
if (previewBtn) {
previewBtn.disabled = true;
previewBtn.style.opacity = '0.5';
previewBtn.style.cursor = 'not-allowed';
}
const count = document.getElementById('liberty-repeat-count')?.value || 3;
const precision = document.getElementById('liberty-precision')?.value || 70;
@ -1034,12 +1033,24 @@ class ConceptionAssistant {
`);
// Use EventSource for streaming
await this.handleLibertyModeStreaming(fullContent, count, precision);
try {
await this.handleLibertyModeStreaming(fullContent, count, precision);
} finally {
// Re-enable preview button after completion (even on error)
if (previewBtn) {
previewBtn.disabled = false;
previewBtn.style.opacity = '1';
previewBtn.style.cursor = 'pointer';
}
}
break;
}
} catch (error) {
console.error('AI Error:', error);
this.showAIFeedback(`<strong>Error</strong><br><br>An error occurred: ${error.message}<br><br>Check your connection and API configuration.`);
} finally {
// Re-enable all AI buttons after completion (even on error)
this.enableAIButtons();
}
}
@ -1324,6 +1335,44 @@ class ConceptionAssistant {
`;
}
disableAIButtons() {
// Disable all AI buttons to prevent API overload
const aiButtons = [
'activate-rephrase',
'check-inconsistencies',
'check-duplications',
'give-advice',
'liberty-mode'
];
aiButtons.forEach(buttonId => {
const btn = document.getElementById(buttonId);
if (btn) {
btn.disabled = true;
btn.style.opacity = '0.5';
btn.style.cursor = 'not-allowed';
}
});
}
enableAIButtons() {
// Re-enable all AI buttons
const aiButtons = [
'activate-rephrase',
'check-inconsistencies',
'check-duplications',
'give-advice',
'liberty-mode'
];
aiButtons.forEach(buttonId => {
const btn = document.getElementById(buttonId);
if (btn) {
btn.disabled = false;
btn.style.opacity = '1';
btn.style.cursor = 'pointer';
}
});
}
ensureEditMode() {
// If in preview mode, force return to edit mode
if (this.isPreviewMode) {
@ -1349,45 +1398,6 @@ class ConceptionAssistant {
this.editor.contentEditable = true;
}
async fixMermaidDiagram(mermaidCode, errorMessage) {
try {
this.showNotification('Fixing Mermaid diagram...', 'info');
const response = await fetch('/api/ai/fix-mermaid', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
mermaidCode: mermaidCode,
error: errorMessage
})
});
const result = await response.json();
if (result.success && result.data.fixedCode) {
// Replace the erroneous Mermaid code in the editor
const currentContent = this.editor.innerText;
const fixedContent = currentContent.replace(mermaidCode, result.data.fixedCode);
this.editor.innerText = fixedContent;
this.generateTOC();
this.saveState(true);
this.showNotification('Mermaid diagram fixed automatically', 'success');
// Return true to indicate successful fix
return true;
} else {
this.showNotification('Could not fix Mermaid diagram', 'error');
return false;
}
} catch (error) {
console.error('Error fixing Mermaid:', error);
this.showNotification('Error fixing Mermaid diagram: ' + error.message, 'error');
return false;
}
}
showNotification(message, type = 'success') {
const notification = document.createElement('div');
notification.className = `notification ${type}`;
@ -1443,6 +1453,25 @@ class ConceptionAssistant {
this.editor.style.border = '';
this.editor.style.borderRadius = '';
// Disable AI buttons and save/load buttons in preview mode
const disabledButtons = [
'activate-rephrase',
'check-inconsistencies',
'check-duplications',
'give-advice',
'liberty-mode',
'save-journal',
'load-journal'
];
disabledButtons.forEach(buttonId => {
const btn = document.getElementById(buttonId);
if (btn) {
btn.disabled = true;
btn.style.opacity = '0.5';
btn.style.cursor = 'not-allowed';
}
});
// Process Mermaid diagrams after rendering
if (typeof mermaid !== 'undefined') {
try {
@ -1478,26 +1507,11 @@ class ConceptionAssistant {
// Render diagram
mermaid.render(uniqueId + '-svg', mermaidCode).then(({ svg }) => {
mermaidDiv.innerHTML = svg;
}).catch(async (err) => {
}).catch((err) => {
console.warn('Mermaid rendering error:', err);
// Automatically try to fix the Mermaid diagram
this.showNotification('Mermaid error detected. Attempting to fix...', 'warning');
const fixed = await this.fixMermaidDiagram(mermaidCode, err.message);
if (fixed) {
// Diagram was fixed, re-toggle preview to show the corrected version
setTimeout(async () => {
// Exit preview mode
await this.togglePreview();
// Re-enter preview mode to render fixed diagram
setTimeout(() => this.togglePreview(), 500);
}, 1000);
} else {
// Show error if fix failed
mermaidDiv.innerHTML = `<pre style="color: var(--accent-color); padding: 1rem; background: var(--background-color); border-radius: 6px;">Mermaid rendering error: ${err.message}\n\nAutomatic fix failed. Please check the syntax manually.</pre>`;
}
// Show error inline
mermaidDiv.innerHTML = `<pre style="color: var(--accent-color); padding: 1rem; background: var(--background-color); border-radius: 6px; border-left: 4px solid var(--accent-color);">Mermaid rendering error: ${err.message}\n\nPlease check the diagram syntax in the editor.</pre>`;
});
} catch (error) {
console.warn('Mermaid processing error:', error);
@ -1531,6 +1545,25 @@ class ConceptionAssistant {
mainElement.classList.remove('preview-mode');
}
// Re-enable AI buttons and save/load buttons
const enabledButtons = [
'activate-rephrase',
'check-inconsistencies',
'check-duplications',
'give-advice',
'liberty-mode',
'save-journal',
'load-journal'
];
enabledButtons.forEach(buttonId => {
const btn = document.getElementById(buttonId);
if (btn) {
btn.disabled = false;
btn.style.opacity = '1';
btn.style.cursor = 'pointer';
}
});
// Change button
previewBtn.innerHTML = 'Preview';
previewBtn.classList.remove('secondary');

View File

@ -36,6 +36,138 @@ const httpsAgent = new https.Agent({
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();
@ -377,23 +509,22 @@ router.post('/liberty-mode', checkAIEnabled, async (req, res) => {
- The next header of EQUAL or HIGHER level appears
- The end of the document is reached
Example structure:
```
# Main Title (level 1)
Example structure (using headers with hash symbols):
Level 1 header: Main Title
content...
## Section A (level 2) Section A starts here
Level 2 header: Section A Section A starts here
content A...
### Subsection A1 (level 3) Part of Section A
Level 3 header: Subsection A1 Part of Section A
content A1...
### Subsection A2 (level 3) Part of Section A
Level 3 header: Subsection A2 Part of Section A
content A2...
## Section B (level 2) Section A ends, Section B starts
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"
@ -433,8 +564,8 @@ router.post('/liberty-mode', checkAIEnabled, async (req, res) => {
- **Improvements Made**: Detailed list of enhancements
- **Next Recommendations**: Sections to consider for future iterations
Then, write the complete markdown document inside a code block labeled "document".
The document must include ALL sections, with ONLY the selected section modified.
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}
@ -449,7 +580,35 @@ router.post('/liberty-mode', checkAIEnabled, async (req, res) => {
\`\`\`
\`\`\`document
[Full document with only API Documentation section improved]
### 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}
@ -468,6 +627,10 @@ router.post('/liberty-mode', checkAIEnabled, async (req, res) => {
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/;
@ -475,10 +638,23 @@ router.post('/liberty-mode', checkAIEnabled, async (req, res) => {
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) {
const oldDocumentRegex = /```document\s*([\s\S]*?)```/;
// 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 = '';
@ -491,7 +667,7 @@ router.post('/liberty-mode', checkAIEnabled, async (req, res) => {
// Extract selected section name from comment
const sectionMatch = explanation.match(/\*\*Selected Section\*\*:?\s*(.+?)(?:\n|\*\*|$)/i);
if (sectionMatch && sectionMatch[1]) {
selectedSection = sectionMatch[1].trim();
selectedSection = cleanMarkdownFromTitle(sectionMatch[1].trim());
// Add to modified sections tracker
if (!modifiedSections.includes(selectedSection)) {
modifiedSections.push(selectedSection);
@ -500,9 +676,27 @@ router.post('/liberty-mode', checkAIEnabled, async (req, res) => {
}
if (documentMatch && documentMatch[1]) {
newMarkdown = documentMatch[1].trim();
// Update for next iteration
currentContent = newMarkdown;
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

View File

@ -127,57 +127,15 @@ router.post('/present', (req, res) => {
// Render diagram
mermaid.render(uniqueId + '-svg', mermaidCode).then(function(result) {
mermaidDiv.innerHTML = result.svg;
}).catch(async function(err) {
}).catch(function(err) {
console.warn('Mermaid rendering error:', err);
// Show error message with auto-fix option
// Show error message inline
mermaidDiv.innerHTML = '<div style="padding: 1.5rem; background: #fff3cd; border: 2px solid #ffc107; border-radius: 8px; margin: 1rem 0;">' +
'<h4 style="margin-top: 0; color: #856404;">[!] Mermaid Diagram Error</h4>' +
'<p style="margin: 0.5rem 0;"><strong>Error:</strong> ' + err.message + '</p>' +
'<p style="margin: 1rem 0 0.5rem 0; color: #666;">Attempting to fix automatically...</p>' +
'<div id="fix-status-' + uniqueId + '" style="margin-top: 0.5rem;"></div>' +
'<p style="margin: 1rem 0 0.5rem 0; color: #666;">Please check the diagram syntax in the editor.</p>' +
'</div>';
const statusDiv = document.getElementById('fix-status-' + uniqueId);
try {
// Call AI API to fix Mermaid diagram
const response = await fetch('/api/ai/fix-mermaid', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
mermaidCode: mermaidCode,
error: err.message
})
});
const result = await response.json();
if (result.success && result.data.fixedCode) {
statusDiv.innerHTML = '<p style="color: #28a745; font-weight: 500;">[OK] Diagram fixed! Please close this window and reopen presentation mode from the editor to see the corrected diagram.</p>';
// Try to update parent window if available
if (window.opener && !window.opener.closed) {
try {
// Send message to parent window to update content
window.opener.postMessage({
type: 'mermaid-fixed',
originalCode: mermaidCode,
fixedCode: result.data.fixedCode
}, '*');
statusDiv.innerHTML += '<p style="color: #28a745; margin-top: 0.5rem;">Document updated in editor. You can now close this window and reopen presentation mode.</p>';
} catch (e) {
console.warn('Could not update parent window:', e);
}
}
} else {
statusDiv.innerHTML = '<p style="color: #dc3545;">[ERROR] Automatic fix failed. Please check the syntax in the editor.</p>';
}
} catch (error) {
console.error('Error fixing Mermaid:', error);
statusDiv.innerHTML = '<p style="color: #dc3545;">[ERROR] Could not fix diagram automatically. Please check your syntax in the editor.</p>';
}
});
} catch (error) {
console.warn('Mermaid processing error:', error);