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 = 'Error: ' + err.message + '
' + - 'Attempting to fix automatically...
' + - '' + + 'Please check the diagram syntax in the editor.
' + '[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);