From e3debb3f710806a2b48cb28d69ea5107c5d303c4 Mon Sep 17 00:00:00 2001 From: Muyue Date: Wed, 15 Oct 2025 14:22:40 +0200 Subject: [PATCH] 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. --- assets/js/app.js | 191 ++++++++++++++++++++++----------------- routes/ai.js | 226 +++++++++++++++++++++++++++++++++++++++++++---- routes/index.js | 48 +--------- 3 files changed, 325 insertions(+), 140 deletions(-) diff --git a/assets/js/app.js b/assets/js/app.js index 6352359..b8c31df 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -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(`Error

An error occurred: ${error.message}

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 = `
Mermaid rendering error: ${err.message}\n\nAutomatic fix failed. Please check the syntax manually.
`; - } + // Show error inline + mermaidDiv.innerHTML = `
Mermaid rendering error: ${err.message}\n\nPlease check the diagram syntax in the editor.
`; }); } 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'); diff --git a/routes/ai.js b/routes/ai.js index fff035d..779ab96 100644 --- a/routes/ai.js +++ b/routes/ai.js @@ -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 diff --git a/routes/index.js b/routes/index.js index a5a2d49..828e691 100644 --- a/routes/index.js +++ b/routes/index.js @@ -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 = '
' + '

[!] Mermaid Diagram Error

' + '

Error: ' + err.message + '

' + - '

Attempting to fix automatically...

' + - '
' + + '

Please check the diagram syntax in the editor.

' + '
'; - - 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 = '

[OK] Diagram fixed! Please close this window and reopen presentation mode from the editor to see the corrected diagram.

'; - - // 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 += '

Document updated in editor. You can now close this window and reopen presentation mode.

'; - } catch (e) { - console.warn('Could not update parent window:', e); - } - } - } else { - statusDiv.innerHTML = '

[ERROR] Automatic fix failed. Please check the syntax in the editor.

'; - } - } catch (error) { - console.error('Error fixing Mermaid:', error); - statusDiv.innerHTML = '

[ERROR] Could not fix diagram automatically. Please check your syntax in the editor.

'; - } }); } catch (error) { console.warn('Mermaid processing error:', error);