// Main application
class ConceptionAssistant {
constructor() {
this.currentJournalId = null;
this.editor = null;
this.undoStack = [];
this.redoStack = [];
this.tocTimer = null;
this.isPreviewMode = false;
this.originalContent = '';
this.init();
}
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');
// Generate TOC with debounce to avoid interrupting typing
this.editor.addEventListener('input', () => {
this.debounceTOC();
this.updateStatistics();
});
// Keyboard shortcut handling
this.editor.addEventListener('keydown', (e) => {
// Ctrl+S to save
if (e.ctrlKey && e.key === 's') {
e.preventDefault();
this.saveJournal();
}
// Ctrl+Z to undo
if (e.ctrlKey && e.key === 'z' && !e.shiftKey) {
e.preventDefault();
this.undo();
}
// Ctrl+Y or Ctrl+Shift+Z to redo
if (e.ctrlKey && (e.key === 'y' || (e.shiftKey && e.key === 'Z'))) {
e.preventDefault();
this.redo();
}
// Tab for indentation
if (e.key === 'Tab') {
e.preventDefault();
document.execCommand('insertText', false, ' ');
}
});
// Save state for undo/redo before important modifications
this.editor.addEventListener('input', () => {
this.saveState();
});
}
showEditorPlaceholder() {
// Placeholder is now managed via CSS ::before
// This function can be removed or left empty for compatibility
}
setupEventListeners() {
// Journal control buttons
document.getElementById('save-journal')?.addEventListener('click', () => this.saveJournal());
document.getElementById('load-journal')?.addEventListener('click', () => this.showJournalSelector());
document.getElementById('preview-toggle')?.addEventListener('click', async () => await this.togglePreview());
document.getElementById('new-blank-document')?.addEventListener('click', () => this.createBlankDocument());
// Table of contents
document.getElementById('refresh-toc')?.addEventListener('click', () => this.generateTOC());
// Export/Import
document.getElementById('export-md')?.addEventListener('click', () => this.exportMarkdown());
document.getElementById('export-web')?.addEventListener('click', () => this.exportWeb());
document.getElementById('import-md')?.addEventListener('change', (e) => this.importMarkdown(e));
// Theme
document.getElementById('theme-toggle')?.addEventListener('click', () => this.toggleTheme());
// AI Assistant
document.getElementById('activate-rephrase')?.addEventListener('click', () => this.handleAI('rephrase'));
document.getElementById('check-inconsistencies')?.addEventListener('click', () => this.handleAI('inconsistencies'));
document.getElementById('check-duplications')?.addEventListener('click', () => this.handleAI('duplications'));
document.getElementById('give-advice')?.addEventListener('click', () => this.handleAI('advice'));
document.getElementById('liberty-mode')?.addEventListener('click', () => this.handleAI('liberty'));
// Present mode
document.getElementById('present-mode')?.addEventListener('click', () => this.openPresentMode());
// Shortcuts help
document.getElementById('show-shortcuts')?.addEventListener('click', () => this.showShortcutsModal());
// Loading modal
document.getElementById('close-journal-modal')?.addEventListener('click', () => this.closeModal());
// Close modal by clicking on overlay
document.getElementById('journal-modal')?.addEventListener('click', (e) => {
if (e.target.id === 'journal-modal') {
this.closeModal();
}
});
// Scroll to top button
document.getElementById('scroll-to-top')?.addEventListener('click', () => this.scrollToTop());
// Manage scroll to top button display
this.setupScrollToTop();
}
setupScrollToTop() {
const scrollButton = document.getElementById('scroll-to-top');
if (!scrollButton) return;
window.addEventListener('scroll', () => {
if (window.scrollY > 300) {
scrollButton.classList.add('visible');
} else {
scrollButton.classList.remove('visible');
}
});
}
scrollToTop() {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
this.showNotification('Back to top', 'success');
}
saveState(immediate = false) {
// If immediate is true, save immediately without debounce
if (immediate) {
this.performSaveState();
return;
}
// Avoid saving too often (debounce)
if (this.saveStateTimer) {
clearTimeout(this.saveStateTimer);
}
this.saveStateTimer = setTimeout(() => {
this.performSaveState();
}, 1000); // Save after 1 second of inactivity
}
performSaveState() {
const currentContent = this.editor.innerText;
// Don't save if content hasn't changed
if (this.undoStack.length > 0 && this.undoStack[this.undoStack.length - 1] === currentContent) {
return;
}
this.undoStack.push(currentContent);
// Limit undo stack to 50 elements
if (this.undoStack.length > 50) {
this.undoStack.shift();
}
// Clear redo stack because we've done a new action
this.redoStack = [];
}
undo() {
if (this.undoStack.length > 1) {
const currentContent = this.undoStack.pop();
this.redoStack.push(currentContent);
const previousContent = this.undoStack[this.undoStack.length - 1];
this.editor.innerText = previousContent;
// Regenerate table of contents
this.generateTOC();
this.showNotification('Undo completed', 'success');
} else {
this.showNotification('Nothing to undo', 'warning');
}
}
redo() {
if (this.redoStack.length > 0) {
const nextContent = this.redoStack.pop();
this.undoStack.push(nextContent);
this.editor.innerText = nextContent;
// Regenerate table of contents
this.generateTOC();
this.showNotification('Redo completed', 'success');
} else {
this.showNotification('Nothing to redo', 'warning');
}
}
async saveJournal() {
const content = this.editor.innerText;
if (!content) return;
const statusEl = document.getElementById('save-status');
const saveBtn = document.getElementById('save-journal');
saveBtn.classList.add('loading');
statusEl.textContent = 'Saving...';
try {
let response;
if (this.currentJournalId) {
// Update
response = await fetch(`/api/journals/${this.currentJournalId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content })
});
} else {
// Create
response = await fetch('/api/journals', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content })
});
}
const result = await response.json();
if (result.success) {
if (!this.currentJournalId) {
this.currentJournalId = result.data.id;
}
statusEl.textContent = 'Saved';
this.showNotification('Journal saved successfully', 'success');
setTimeout(() => statusEl.textContent = '', 3000);
} else {
throw new Error(result.error || 'Save error');
}
} catch (error) {
console.error('Error:', error);
statusEl.textContent = 'Error';
this.showNotification('Error while saving: ' + error.message, 'error');
setTimeout(() => statusEl.textContent = '', 3000);
} finally {
saveBtn.classList.remove('loading');
}
}
async loadJournalList() {
try {
const response = await fetch('/api/journals');
const result = await response.json();
if (result.success && result.data.length > 0) {
// Load the last journal automatically
const lastJournal = result.data[result.data.length - 1];
await this.loadJournal(lastJournal.id);
}
} catch (error) {
console.error('Error loading list:', error);
}
}
async loadJournal(id) {
try {
const response = await fetch(`/api/journals/${id}`);
const result = await response.json();
if (result.success && result.data.length > 0) {
const journal = result.data[0];
this.currentJournalId = id;
this.editor.innerText = journal.markdownContent;
this.generateTOC();
// Reset history for the new journal
this.undoStack = [journal.markdownContent];
this.redoStack = [];
// Ensure editor is in edit mode
this.ensureEditMode();
this.showNotification('Journal loaded', 'success');
}
} catch (error) {
console.error('Error loading journal:', error);
this.showNotification('Error while loading', 'error');
}
}
async showJournalSelector() {
try {
const response = await fetch('/api/journals');
const result = await response.json();
if (result.success) {
const journalList = result.data.map(journal => {
// Extract first lines for preview
const lines = journal.markdownContent.split('\n');
const firstLines = lines.slice(0, 3).join('\n');
const preview = firstLines.length > 150 ? firstLines.substring(0, 150) + '...' : firstLines;
return `
Journal ${journal.id}
${preview}
`;
}).join('');
const modalBody = document.getElementById('journal-modal-body');
modalBody.innerHTML = `
+ New Journal
${journalList}
`;
// Show modal with animation
const modal = document.getElementById('journal-modal');
modal.style.display = 'flex';
setTimeout(() => modal.classList.add('show'), 10);
// Add event listeners
document.querySelectorAll('.journal-item').forEach(btn => {
btn.addEventListener('click', () => {
this.loadJournal(btn.dataset.id);
this.closeModal();
});
});
}
} catch (error) {
console.error('Error:', error);
this.showNotification('Error loading list', 'error');
}
}
closeModal() {
const modal = document.getElementById('journal-modal');
modal.classList.remove('show');
setTimeout(() => {
modal.style.display = 'none';
}, 300);
}
createNewJournal() {
this.currentJournalId = null;
this.editor.innerText = '';
this.generateTOC();
this.clearFeedback();
// Reset history for new journal
this.undoStack = [''];
this.redoStack = [];
// Ensure editor is in edit mode
this.ensureEditMode();
this.showNotification('New journal created', 'success');
}
createBlankDocument() {
// Clear the editor completely
this.editor.innerText = '';
// Reset currentJournalId to null
this.currentJournalId = null;
// Reset undo/redo stacks
this.undoStack = [''];
this.redoStack = [];
// Generate TOC
this.generateTOC();
// Update statistics
this.updateStatistics();
// Show notification
this.showNotification('New blank document created', 'success');
// Close all panels
closeAllPanels();
// Ensure editor is in edit mode
this.ensureEditMode();
}
debounceTOC() {
if (this.tocTimer) {
clearTimeout(this.tocTimer);
}
this.tocTimer = setTimeout(() => {
this.generateTOC();
}, 500); // Wait 500ms after last keystroke
}
saveSelection() {
const selection = window.getSelection();
if (selection.rangeCount > 0) {
return selection.getRangeAt(0).cloneRange();
}
return null;
}
restoreSelection(range) {
if (range) {
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
}
}
cleanMarkdownFromTitle(title) {
// Remove all markdown formatting markers from title
return title
.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();
}
generateTOC() {
// Save cursor position
const savedRange = this.saveSelection();
const content = this.editor.innerText;
const lines = content.split('\n');
const toc = [];
// Remove existing anchors
const existingAnchors = this.editor.querySelectorAll('.heading-anchor');
existingAnchors.forEach(anchor => anchor.remove());
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line.startsWith('#')) {
const level = line.match(/^#+/)[0].length;
const title = line.replace(/^#+\s*/, '');
const id = 'heading-' + i;
toc.push({ level, title, id, lineIndex: i });
}
}
let tocHtml = '';
if (toc.length > 0) {
// Nouvelle génération de TOC avec indentation correcte
let previousLevel = 0;
for (let i = 0; i < toc.length; i++) {
const item = toc[i];
// Ouvrir les listes nécessaires
if (item.level > previousLevel) {
for (let j = previousLevel; j < item.level; j++) {
tocHtml += '';
}
}
// Fermer les listes nécessaires
else if (item.level < previousLevel) {
for (let j = item.level; j < previousLevel; j++) {
tocHtml += ' ';
}
}
// Même niveau, fermer le li précédent
else if (i > 0) {
tocHtml += '';
}
// Ajouter l'élément avec indentation CSS
const indent = (item.level - 1) * 1; // 1rem par niveau
const cleanTitle = this.cleanMarkdownFromTitle(item.title);
tocHtml += `${cleanTitle} `;
previousLevel = item.level;
}
// Fermer toutes les listes restantes
for (let j = 0; j < previousLevel; j++) {
tocHtml += ' ';
}
} else {
tocHtml = 'Add headings (# ## ###) to your journal to generate the table of contents.
';
}
document.getElementById('toc-nav').innerHTML = tocHtml;
// Restore cursor position
this.restoreSelection(savedRange);
}
addHeadingAnchors(lines, toc) {
const editorLines = this.editor.innerHTML.split(' ');
let newContent = '';
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const tocItem = toc.find(item => item.lineIndex === i);
if (tocItem) {
newContent += ` `;
}
newContent += line;
if (i < lines.length - 1) newContent += ' ';
}
this.editor.innerHTML = newContent;
}
scrollToHeading(title) {
try {
const content = this.editor.innerText;
const lines = content.split('\n');
// Find the line index and calculate character position
let targetLineIndex = -1;
let charPosition = 0;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line.startsWith('#') && line.replace(/^#+\s*/, '') === title) {
targetLineIndex = i;
break;
}
// Count characters including newlines
charPosition += lines[i].length + 1; // +1 for \n
}
if (targetLineIndex === -1) {
this.showNotification('Section not found', 'warning');
return;
}
// Create a range at the target position to get accurate coordinates
const selection = window.getSelection();
const range = document.createRange();
// Find the text node at the character position
const walker = document.createTreeWalker(
this.editor,
NodeFilter.SHOW_TEXT,
null
);
let currentChar = 0;
let targetNode = null;
let offsetInNode = 0;
while (walker.nextNode()) {
const node = walker.currentNode;
const nodeLength = node.textContent.length;
if (currentChar + nodeLength >= charPosition) {
targetNode = node;
offsetInNode = charPosition - currentChar;
break;
}
currentChar += nodeLength;
}
if (targetNode) {
// Set range at the target position
range.setStart(targetNode, Math.min(offsetInNode, targetNode.textContent.length));
range.setEnd(targetNode, Math.min(offsetInNode, targetNode.textContent.length));
// Get the bounding rectangle of the range
const rect = range.getBoundingClientRect();
// Scroll to the element - use the parent container (#design-journal)
const journalSection = document.getElementById('design-journal');
const sectionRect = journalSection.getBoundingClientRect();
const scrollOffset = rect.top - sectionRect.top + journalSection.scrollTop - 100;
journalSection.scrollTo({
top: Math.max(0, scrollOffset),
behavior: 'smooth'
});
// Temporarily highlight the heading
selection.removeAllRanges();
selection.addRange(range);
setTimeout(() => selection.removeAllRanges(), 1000);
}
this.showNotification(`Navigating to: ${title}`, 'success');
} catch (error) {
console.error('Scroll error:', error);
this.showNotification('Error during navigation', 'error');
}
}
highlightHeading(title) {
// Function to temporarily highlight the found title
try {
const content = this.editor.innerText;
const lines = content.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line.startsWith('#') && line.replace(/^#+\s*/, '') === title) {
// Create a range to select the line
const selection = window.getSelection();
const range = document.createRange();
// Find the text node and position
const walker = document.createTreeWalker(
this.editor,
NodeFilter.SHOW_TEXT,
null,
false
);
let currentLine = 0;
let textNode = walker.nextNode();
while (textNode && currentLine < i) {
const nodeText = textNode.textContent;
const newLines = (nodeText.match(/\n/g) || []).length;
currentLine += newLines;
if (currentLine < i) {
textNode = walker.nextNode();
}
}
if (textNode) {
// Find the start of the line in this node
const nodeText = textNode.textContent;
const linesInNode = nodeText.split('\n');
const targetLineInNode = i - (currentLine - linesInNode.length + 1);
if (targetLineInNode >= 0 && targetLineInNode < linesInNode.length) {
let startPos = 0;
for (let j = 0; j < targetLineInNode; j++) {
startPos += linesInNode[j].length + 1;
}
const endPos = startPos + linesInNode[targetLineInNode].length;
range.setStart(textNode, startPos);
range.setEnd(textNode, endPos);
// Select temporarily
selection.removeAllRanges();
selection.addRange(range);
// Remove selection after a short delay
setTimeout(() => {
selection.removeAllRanges();
}, 1000);
}
}
break;
}
}
} catch (error) {
// Ignore highlight errors, not critical
console.log('Highlight error:', error);
}
}
exportMarkdown() {
const content = this.editor.innerText;
if (!content.trim()) {
this.showNotification('No content to export', 'warning');
return;
}
const blob = new Blob([content], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'design-journal.md';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
this.showNotification('Markdown file exported', 'success');
}
async exportWeb() {
const content = this.editor.innerText;
if (!content.trim()) {
this.showNotification('No content to export', 'warning');
return;
}
try {
this.showNotification('Generating web export...', 'success');
const response = await fetch('/api/export/web', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content, title: 'Design Journal' })
});
if (!response.ok) {
throw new Error('Export failed');
}
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'design-journal-web.zip';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
this.showNotification('Web export (ZIP) downloaded', 'success');
} catch (error) {
console.error('Export error:', error);
this.showNotification('Error exporting web: ' + error.message, 'error');
}
}
importMarkdown(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
const importedContent = e.target.result;
this.editor.innerText = importedContent;
this.generateTOC();
this.currentJournalId = null; // New journal
// Reset history for imported file
this.undoStack = [importedContent];
this.redoStack = [];
// Ensure editor is in edit mode
this.ensureEditMode();
this.showNotification('Markdown file imported', 'success');
};
reader.readAsText(file);
}
toggleTheme() {
document.body.classList.toggle('dark-theme');
const isDark = document.body.classList.contains('dark-theme');
localStorage.setItem('theme', isDark ? 'dark' : 'light');
this.showNotification(`${isDark ? 'Dark' : 'Light'} mode activated`, 'success');
}
updateStatistics() {
const content = this.editor.innerText.trim();
// Count words
const words = content.split(/\s+/).filter(word => word.length > 0);
const wordCount = words.length;
// Count characters
const charCount = content.length;
// Calculate reading time (200 words per minute)
const readingTime = Math.ceil(wordCount / 200);
// Update DOM elements
const wordCountEl = document.getElementById('word-count');
const charCountEl = document.getElementById('char-count');
const readingTimeEl = document.getElementById('reading-time');
if (wordCountEl) wordCountEl.textContent = wordCount;
if (charCountEl) charCountEl.textContent = charCount;
if (readingTimeEl) readingTimeEl.textContent = `${readingTime} min`;
}
async openPresentMode() {
const content = this.editor.innerText;
if (!content.trim()) {
this.showNotification('No content to present', 'warning');
return;
}
try {
this.showNotification('Opening presentation mode...', 'success');
// Create a form to POST the content
const form = document.createElement('form');
form.method = 'POST';
form.action = '/present';
form.target = '_blank';
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'content';
input.value = content;
form.appendChild(input);
document.body.appendChild(form);
form.submit();
document.body.removeChild(form);
} catch (error) {
console.error('Present mode error:', error);
this.showNotification('Error opening presentation mode', 'error');
}
}
showShortcutsModal() {
const modalBody = document.getElementById('journal-modal-body');
const modalHeader = document.querySelector('#journal-modal .modal-header h3');
if (modalHeader) {
modalHeader.textContent = 'Keyboard Shortcuts';
}
modalBody.innerHTML = `
Document Management
Ctrl+S
Save journal
Ctrl+Z
Undo
Ctrl+Y
Redo
Tab
Indent text
Esc
Close side panels
Markdown Formatting
# Title
Heading level 1
## Title
Heading level 2
**bold**
Bold text
*italic*
Italic text
- item
Bullet list
\`code\`
Inline code
Close
`;
// Show modal
const modal = document.getElementById('journal-modal');
modal.style.display = 'flex';
setTimeout(() => modal.classList.add('show'), 10);
}
async handleAI(action) {
const selection = window.getSelection().toString().trim();
const fullContent = this.editor.innerText;
// Check conditions based on action
if (action === 'rephrase' && !selection) {
this.showAIFeedback('Please select text to rephrase');
return;
}
// Determine content to use based on action
let contentToUse;
switch (action) {
case 'rephrase':
contentToUse = selection;
break;
case 'inconsistencies':
case 'duplications':
case 'advice':
case 'liberty':
contentToUse = selection || fullContent;
break;
default:
contentToUse = fullContent;
}
// Show loading message
this.showAIFeedback('Processing...
');
try {
let result;
switch (action) {
case 'rephrase':
result = await this.callAI('/api/ai/rephrase', { text: selection.trim(), context: fullContent.substring(0, 500) });
// Get rephrased text directly
const rephrasedText = result.rephrased || result.data || result;
// Store suggestion for validation
this.lastRephraseData = {
original: selection.trim(),
rephrased: rephrasedText,
selection: window.getSelection()
};
this.showAIFeedback(`
Rephrasing
Original text:
${selection.substring(0, 200)}${selection.length > 200 ? '...' : ''}
Improved version:
${rephrasedText}
Apply rephrasing
Cancel
`);
// Add event listeners for buttons
document.getElementById('validate-rephrase')?.addEventListener('click', () => this.validateRephrase());
document.getElementById('cancel-rephrase')?.addEventListener('click', () => this.clearFeedback());
break;
case 'inconsistencies':
result = await this.callAI('/api/ai/check-inconsistencies', { content: contentToUse });
this.showAIFeedback(`Inconsistency Analysis ${this.formatAIResponse(result.analysis)}`);
break;
case 'duplications':
result = await this.callAI('/api/ai/check-duplications', { content: contentToUse });
this.showAIFeedback(`Duplication Check ${this.formatAIResponse(result.analysis)}`);
break;
case 'advice':
result = await this.callAI('/api/ai/give-advice', { content: contentToUse, domain: 'design' });
this.showAIFeedback(`Improvement Advice ${this.formatAIResponse(result.advice)}`);
break;
case 'liberty':
// Save state before AI modifications
this.saveState(true);
const count = document.getElementById('liberty-repeat-count')?.value || 3;
const precision = document.getElementById('liberty-precision')?.value || 70;
// Initialize progress display
this.showAIFeedback(`
Enhanced Mode
${count} iterations - Precision: ${precision}% - Focus: design
`);
// Use EventSource for streaming
await this.handleLibertyModeStreaming(fullContent, count, precision);
break;
}
} catch (error) {
console.error('AI Error:', error);
this.showAIFeedback(`Error An error occurred: ${error.message} Check your connection and API configuration.`);
}
}
async handleLibertyModeStreaming(content, iterations, precision) {
return new Promise((resolve, reject) => {
// Prepare data to send
const requestData = {
content: content,
iterations: iterations,
precision: precision,
focus: 'design'
};
// Create fetch request for streaming
fetch('/api/ai/liberty-mode', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestData)
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
const processStream = () => {
return reader.read().then(({ done, value }) => {
if (done) {
resolve();
return;
}
// Decode received data
buffer += decoder.decode(value, { stream: true });
// Process each received line
const lines = buffer.split('\n');
buffer = lines.pop() || ''; // Keep incomplete last line
for (const line of lines) {
if (line.trim().startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6)); // Remove "data: "
this.handleLibertyStreamData(data);
} catch (e) {
console.error('JSON parsing error:', e);
}
}
}
return processStream();
});
};
return processStream();
})
.catch(error => {
console.error('Streaming error:', error);
this.showAIFeedback(`Error Streaming error: ${error.message}`);
reject(error);
});
});
}
handleLibertyStreamData(data) {
const progressFill = document.getElementById('liberty-progress-fill');
const iterationsDiv = document.getElementById('liberty-iterations');
if (data.error) {
// Display error
const errorHTML = `
Error: ${data.error}
`;
iterationsDiv.innerHTML += errorHTML;
return;
}
if (data.iteration) {
// Update progress bar
const totalIterations = parseInt(document.getElementById('liberty-repeat-count')?.value || 3);
const progressPercent = (data.iteration / totalIterations) * 100;
if (progressFill) {
progressFill.style.width = `${progressPercent}%`;
}
// Display this iteration's explanation with selected section
const sectionBadge = data.selectedSection
? `${data.selectedSection} `
: '';
const iterationHTML = `
Iteration ${data.iteration} ${sectionBadge}
${this.formatAIResponse(data.explanation)}
`;
iterationsDiv.innerHTML += iterationHTML;
// Update editor with new markdown if available
if (data.markdown && data.markdown !== this.editor.innerText) {
this.editor.innerText = data.markdown;
this.generateTOC();
}
// Scroll to bottom of feedback to see new iteration
const feedback = document.getElementById('ai-assistant-feedback');
feedback.scrollTop = feedback.scrollHeight;
}
if (data.completed) {
// Finalize display
if (progressFill) {
progressFill.style.width = '100%';
}
if (data.finalMarkdown) {
// Ensure final content is in editor
this.editor.innerText = data.finalMarkdown;
this.generateTOC();
// Save final state
this.saveState(true);
}
// Completion message
const completedHTML = `
Enhanced Mode completed!
${data.totalIterations || 'All'} iteration(s) completed
`;
iterationsDiv.innerHTML += completedHTML;
this.showNotification('Enhanced Mode completed', 'success');
}
}
async callAI(endpoint, data) {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
const result = await response.json();
if (!result.success) {
throw new Error(result.error || 'Unknown error');
}
return result.data;
}
formatAIResponse(text) {
if (!text) return '';
// Parser la réponse pour séparer le raisonnement du contenu principal
const sections = text.split('##');
let formattedHTML = '';
const self = this;
sections.forEach((section, index) => {
if (index === 0 && section.trim()) {
// Première section sans titre
formattedHTML += `${self.parseMarkdown(section.trim())}
`;
} else if (section.trim()) {
const lines = section.split('\n');
const title = lines[0].trim();
const content = lines.slice(1).join('\n').trim();
// Sections normales - avec markdown
formattedHTML += `
${title}
${self.parseMarkdown(content)}
`;
}
});
return formattedHTML || text;
}
parseMarkdown(text) {
if (!text) return '';
return text
// Titres H1-H6 (#, ##, ###, etc.)
.replace(/^(#{1,6})\s+(.+)$/gm, (match, hashes, title) => {
const level = hashes.length;
return `${title} `;
})
// Listes à puces (- ou *)
.replace(/^[\s]*[-\*]\s+(.+)$/gm, '$1 ')
// Listes numérotées
.replace(/^[\s]*\d+\.\s+(.+)$/gm, '$1 ')
// Code blocks avec ```
.replace(/```([\s\S]*?)```/g, '$1 ')
// Citations avec >
.replace(/^>\s+(.+)$/gm, '$1 ')
// Gras **texte**
.replace(/\*\*(.*?)\*\*/g, '$1 ')
// Italique *texte*
.replace(/\*(.*?)\*/g, '$1 ')
// Code inline `code`
.replace(/`([^`]+)`/g, '$1')
// Liens [texte](url)
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1 ')
// Séparer les listes en blocs ou
.replace(/(]*>.*<\/li>)/gs, (match) => {
if (match.includes(' ${match} `;
}
return match;
})
// Sauts de ligne doubles pour paragraphes
.replace(/\n\n+/g, '\n\n')
.replace(/\n\n/g, '')
// Sauts de ligne simples
.replace(/\n/g, ' ')
// Encapsuler dans un paragraphe si pas déjà fait
.replace(/^(?!<[h1-6|ul|ol|pre|blockquote])/i, '
')
.replace(/$/i, '
');
}
showAIFeedback(message) {
const feedback = document.getElementById('ai-assistant-feedback');
feedback.innerHTML = `${message}
`;
// Scroll to top of feedback to see result
feedback.scrollTop = 0;
}
validateRephrase() {
if (!this.lastRephraseData) return;
try {
// Save state before rephrasing to allow undo
this.saveState(true);
// Replace text in editor
const range = this.lastRephraseData.selection.getRangeAt(0);
range.deleteContents();
range.insertNode(document.createTextNode(this.lastRephraseData.rephrased));
// Clear selection and regenerate TOC
window.getSelection().removeAllRanges();
this.generateTOC();
// Save state after rephrasing
this.saveState(true);
// Show success message
this.showNotification('Rephrasing applied successfully', 'success');
this.clearFeedback();
// Clean up data
this.lastRephraseData = null;
} catch (error) {
console.error('Error applying rephrasing:', error);
this.showNotification('Error applying rephrasing', 'error');
}
}
clearFeedback() {
const feedback = document.getElementById('ai-assistant-feedback');
feedback.innerHTML = `
AI Assistant ready
Select text in the editor and click an action to begin.
`;
}
ensureEditMode() {
// If in preview mode, force return to edit mode
if (this.isPreviewMode) {
const previewBtn = document.getElementById('preview-toggle');
// Return to edit mode without using originalContent because we want new content
this.editor.contentEditable = true;
this.editor.style.background = '';
this.editor.style.border = '';
this.editor.style.borderRadius = '';
// Change button
if (previewBtn) {
previewBtn.innerHTML = 'Preview';
previewBtn.classList.remove('secondary');
previewBtn.classList.add('primary');
}
this.isPreviewMode = false;
}
// Ensure editor is always editable
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}`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => notification.classList.add('show'), 100);
setTimeout(() => {
notification.classList.remove('show');
setTimeout(() => document.body.removeChild(notification), 300);
}, 3000);
}
async togglePreview() {
const previewBtn = document.getElementById('preview-toggle');
const mainElement = document.querySelector('main');
if (!this.isPreviewMode) {
// Switch to preview mode - Save innerText instead of innerHTML
this.originalContent = this.editor.innerText;
const markdownContent = this.editor.innerText;
// Add preview-mode class to main to adjust grid layout
if (mainElement) {
mainElement.classList.add('preview-mode');
}
// Configure Marked for GitHub-compatible rendering
if (typeof marked !== 'undefined') {
marked.setOptions({
breaks: true,
gfm: true,
headerIds: true,
sanitize: false,
smartypants: false
});
}
// Convert Markdown to HTML with Marked (GitHub-compatible)
let previewHTML = '';
if (typeof marked !== 'undefined') {
previewHTML = marked.parse(markdownContent);
} else {
// Fallback to our custom parser if Marked is not loaded
previewHTML = this.parseMarkdown(markdownContent);
}
// Disable editing and apply GitHub preview style
this.editor.contentEditable = false;
this.editor.innerHTML = `${previewHTML}
`;
this.editor.style.background = '';
this.editor.style.border = '';
this.editor.style.borderRadius = '';
// Process Mermaid diagrams after rendering
if (typeof mermaid !== 'undefined') {
try {
mermaid.initialize({
startOnLoad: false,
theme: document.body.classList.contains('dark-theme') ? 'dark' : 'default',
securityLevel: 'loose',
fontFamily: 'system-ui, -apple-system, "Segoe UI", Roboto, sans-serif'
});
// Wait for DOM to be ready to process Mermaid diagrams
setTimeout(() => {
const preview = this.editor.querySelector('.markdown-preview');
if (preview) {
// Find all code blocks with 'mermaid' language
const mermaidBlocks = preview.querySelectorAll('code.language-mermaid, pre code.language-mermaid');
mermaidBlocks.forEach((block, index) => {
try {
const mermaidCode = block.textContent;
const uniqueId = `mermaid-${Date.now()}-${index}`;
// Create div with unique ID for Mermaid
const mermaidDiv = document.createElement('div');
mermaidDiv.id = uniqueId;
mermaidDiv.className = 'mermaid';
mermaidDiv.textContent = mermaidCode;
// Replace code block with Mermaid div
const pre = block.closest('pre') || block;
pre.parentNode.replaceChild(mermaidDiv, pre);
// Render diagram
mermaid.render(uniqueId + '-svg', mermaidCode).then(({ svg }) => {
mermaidDiv.innerHTML = svg;
}).catch(async (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. `;
}
});
} catch (error) {
console.warn('Mermaid processing error:', error);
}
});
}
}, 200);
} catch (error) {
console.warn('Mermaid initialization error:', error);
}
}
// Change button
previewBtn.innerHTML = 'Edit';
previewBtn.classList.remove('primary');
previewBtn.classList.add('secondary');
this.isPreviewMode = true;
this.showNotification('Preview mode activated', 'success');
} else {
// Return to edit mode - Restore innerText instead of innerHTML
this.editor.contentEditable = true;
this.editor.innerText = this.originalContent;
this.editor.style.background = '';
this.editor.style.border = '';
this.editor.style.borderRadius = '';
// Remove preview-mode class to restore normal grid layout
if (mainElement) {
mainElement.classList.remove('preview-mode');
}
// Change button
previewBtn.innerHTML = 'Preview';
previewBtn.classList.remove('secondary');
previewBtn.classList.add('primary');
this.isPreviewMode = false;
this.showNotification('Edit mode activated', 'success');
}
}
}
// Side panel management
function togglePanel(side) {
const panel = document.getElementById(`${side}-panel`);
const overlay = document.getElementById('panel-overlay');
const otherPanel = document.getElementById(side === 'left' ? 'right-panel' : 'left-panel');
// Close the other panel if it's open
if (otherPanel && otherPanel.classList.contains('open')) {
otherPanel.classList.remove('open');
}
// Toggle current panel
if (panel.classList.contains('open')) {
panel.classList.remove('open');
overlay.classList.remove('active');
} else {
panel.classList.add('open');
overlay.classList.add('active');
}
}
function closeAllPanels() {
const leftPanel = document.getElementById('left-panel');
const rightPanel = document.getElementById('right-panel');
const overlay = document.getElementById('panel-overlay');
if (leftPanel) leftPanel.classList.remove('open');
if (rightPanel) rightPanel.classList.remove('open');
if (overlay) overlay.classList.remove('active');
}
// Close panels with Escape
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeAllPanels();
}
});
// Application initialization
let app;
document.addEventListener('DOMContentLoaded', () => {
app = new ConceptionAssistant();
// Load saved theme
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'dark') {
document.body.classList.add('dark-theme');
}
// Initialize panels
initializePanels();
});
function initializePanels() {
// Initialize template management
initializeTemplateForm();
}
function initializeTemplateForm() {
const loadTemplateBtn = document.getElementById('load-template');
// Handle template loading
if (loadTemplateBtn) {
loadTemplateBtn.addEventListener('click', async () => {
try {
loadTemplateBtn.classList.add('loading');
const response = await fetch('/api/templates/default');
const result = await response.json();
if (result.success) {
// Load template into editor
app.editor.innerText = result.data.content;
app.generateTOC();
app.currentJournalId = null; // New journal
// Reset history for new template
app.undoStack = [result.data.content];
app.redoStack = [];
// Ensure editor is in edit mode
app.ensureEditMode();
app.showNotification('Template loaded successfully', 'success');
closeAllPanels();
} else {
app.showNotification('Error loading template', 'error');
}
} catch (error) {
console.error('Error:', error);
app.showNotification('Error loading template', 'error');
} finally {
loadTemplateBtn.classList.remove('loading');
}
});
}
}
// Ensure togglePanel is globally accessible
window.togglePanel = togglePanel;