diff --git a/assets/css/style.css b/assets/css/style.css index 507dc9e..7555e29 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -590,7 +590,11 @@ section h2 { /* Table des matières */ #table-of-contents { - /* Comportement normal de scroll */ + max-height: calc(100vh - 200px); + overflow-y: auto; + position: sticky; + top: 100px; + align-self: flex-start; } #toc-nav { @@ -599,10 +603,12 @@ section h2 { #toc-nav ul { list-style: none; + padding-left: 0; } #toc-nav li { margin-bottom: 0.5rem; + list-style: none; } #toc-nav a { @@ -621,8 +627,9 @@ section h2 { } #toc-nav ul ul { - margin-left: 1rem; + margin-left: 0; margin-top: 0.5rem; + padding-left: 0; } #toc-nav ul ul a { @@ -633,6 +640,8 @@ section h2 { /* Zone d'écriture */ #design-journal { min-height: 600px; + max-height: calc(100vh - 200px); + overflow-y: auto; } #journal-editor { @@ -644,7 +653,6 @@ section h2 { border: none; background: var(--surface-color); color: var(--text-color); - resize: vertical; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; @@ -693,7 +701,10 @@ section h2 { #ai-assistant { display: flex; flex-direction: column; - /* Comportement normal de scroll */ + max-height: calc(100vh - 200px); + position: sticky; + top: 100px; + align-self: flex-start; } /* Styles pour "Coming Soon" */ diff --git a/assets/js/app.js b/assets/js/app.js index 4681ff9..3d0e755 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -434,7 +434,6 @@ class ConceptionAssistant { const content = this.editor.innerText; const lines = content.split('\n'); const toc = []; - let tocHtml = ''; // Remove existing anchors const existingAnchors = this.editor.querySelectorAll('.heading-anchor'); @@ -451,29 +450,42 @@ class ConceptionAssistant { } } - // Temporarily disabled to avoid disrupting typing - // this.addHeadingAnchors(lines, toc); - + let tocHtml = ''; if (toc.length > 0) { - tocHtml = ''; } } + // Même niveau, fermer le li précédent + else if (i > 0) { + tocHtml += ''; + } - tocHtml += `
  • ${item.title}
  • `; - currentLevel = item.level; + // Ajouter l'élément avec indentation CSS + const indent = (item.level - 1) * 1; // 1rem par niveau + tocHtml += `
  • ${item.title}`; + + previousLevel = item.level; } - tocHtml += ''; + // 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.

    '; } @@ -505,18 +517,21 @@ class ConceptionAssistant { scrollToHeading(title) { try { - // Simpler and more robust method: search text directly in editor const content = this.editor.innerText; const lines = content.split('\n'); - // Find the line index corresponding to the title + // 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) { @@ -524,37 +539,56 @@ class ConceptionAssistant { return; } - // Calculate approximate line position - const editorStyles = window.getComputedStyle(this.editor); - const lineHeight = parseFloat(editorStyles.lineHeight) || 20; - const paddingTop = parseFloat(editorStyles.paddingTop) || 0; + // Create a range at the target position to get accurate coordinates + const selection = window.getSelection(); + const range = document.createRange(); - // Calculate scroll position based on line number - const targetScrollPosition = (targetLineIndex * lineHeight) + paddingTop; + // Find the text node at the character position + const walker = document.createTreeWalker( + this.editor, + NodeFilter.SHOW_TEXT, + null + ); - // Determine if editor or window should be scrolled - const editorRect = this.editor.getBoundingClientRect(); - const editorHasScroll = this.editor.scrollHeight > this.editor.clientHeight; + let currentChar = 0; + let targetNode = null; + let offsetInNode = 0; - if (editorHasScroll) { - // Scroll within editor - this.editor.scrollTo({ - top: Math.max(0, targetScrollPosition - 60), - behavior: 'smooth' - }); - } else { - // Scroll entire page - const editorTop = this.editor.offsetTop; - const windowScrollTarget = editorTop + targetScrollPosition - 100; + while (walker.nextNode()) { + const node = walker.currentNode; + const nodeLength = node.textContent.length; - window.scrollTo({ - top: Math.max(0, windowScrollTarget), - behavior: 'smooth' - }); + if (currentChar + nodeLength >= charPosition) { + targetNode = node; + offsetInNode = charPosition - currentChar; + break; + } + currentChar += nodeLength; } - // Optional: temporarily highlight the title - this.highlightHeading(title); + 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'); @@ -1297,8 +1331,8 @@ class ConceptionAssistant { const mainElement = document.querySelector('main'); if (!this.isPreviewMode) { - // Switch to preview mode - this.originalContent = this.editor.innerHTML; + // 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 @@ -1392,9 +1426,9 @@ class ConceptionAssistant { this.showNotification('Preview mode activated', 'success'); } else { - // Return to edit mode + // Return to edit mode - Restore innerText instead of innerHTML this.editor.contentEditable = true; - this.editor.innerHTML = this.originalContent; + this.editor.innerText = this.originalContent; this.editor.style.background = ''; this.editor.style.border = ''; this.editor.style.borderRadius = ''; diff --git a/routes/ai.js b/routes/ai.js index 0112c21..6529c37 100644 --- a/routes/ai.js +++ b/routes/ai.js @@ -1,5 +1,6 @@ const express = require('express'); const router = express.Router(); +const https = require('https'); require('dotenv').config({ path: './config/.env' }); // Mistral AI Configuration @@ -27,8 +28,18 @@ function checkAIEnabled(req, res, next) { next(); } -// Function to call Mistral API +// Create a custom HTTPS agent with longer timeouts +const httpsAgent = new https.Agent({ + keepAlive: true, + timeout: 120000, // 120 seconds + maxSockets: 5 +}); + +// Function to call Mistral API with timeout handling async function callMistralAPI(messages, temperature = null) { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 120000); // 120 seconds timeout + try { const response = await fetch(`${MISTRAL_BASE_URL}/chat/completions`, { method: 'POST', @@ -42,9 +53,13 @@ async function callMistralAPI(messages, temperature = null) { temperature: temperature !== null ? temperature : parseFloat(process.env.AI_TEMPERATURE) || 0.3, max_tokens: parseInt(process.env.AI_MAX_TOKENS) || 35000, top_p: parseFloat(process.env.AI_TOP_P) || 0.85 - }) + }), + signal: controller.signal, + agent: httpsAgent }); + clearTimeout(timeoutId); + if (!response.ok) { const error = await response.text(); throw new Error(`Mistral API Error: ${response.status} - ${error}`); @@ -53,7 +68,20 @@ async function callMistralAPI(messages, temperature = null) { const data = await response.json(); return data.choices[0].message.content; } catch (error) { + clearTimeout(timeoutId); console.error('Mistral API Error:', error); + + // Better error messages + if (error.name === 'AbortError') { + throw new Error('Request timeout: The API took too long to respond (>120s)'); + } + if (error.cause && error.cause.code === 'UND_ERR_HEADERS_TIMEOUT') { + throw new Error('Connection timeout: Unable to connect to Mistral API. Check your network or API endpoint.'); + } + if (error.cause && error.cause.code === 'ECONNREFUSED') { + throw new Error('Connection refused: Cannot reach Mistral API endpoint.'); + } + throw error; } } @@ -307,17 +335,22 @@ router.post('/liberty-mode', checkAIEnabled, async (req, res) => { 3. Develop existing ideas with the allowed creativity 4. Maintain logical structure - MANDATORY RESPONSE IN 2 PARTS SEPARATED BY "---SPLIT---": + MANDATORY RESPONSE FORMAT - Use code blocks with triple backticks: - ## Analysis (Iteration ${i + 1}/${maxIterations}) - [Explain the improvements made, sections added, reasoning] + First, write your comment explaining the improvements inside a code block labeled "comment${i + 1}": + - What sections were added + - What was enhanced + - Reasoning behind changes - ---SPLIT--- + Then, write the complete improved markdown document inside a code block labeled "document". - [THE COMPLETE AND IMPROVED MARKDOWN DOCUMENT - WITHOUT "## Document" TITLE - DIRECTLY THE CONTENT] + Example format: + First block: comment${i + 1} + Second block: document Focus: ${focus} - Precision: ${precisionPercent}%` + Precision: ${precisionPercent}% + Iteration: ${i + 1}/${maxIterations}` }, { role: 'user', @@ -330,20 +363,36 @@ router.post('/liberty-mode', checkAIEnabled, async (req, res) => { const result = await callMistralAPI(messages, temperature); - // Separate explanation from markdown - const parts = result.split('---SPLIT---'); + // Extract code blocks + const commentRegex = /```comment\d+\s*([\s\S]*?)```/; + const documentRegex = /```document\s*([\s\S]*?)```/; + + const commentMatch = result.match(commentRegex); + const documentMatch = result.match(documentRegex); let explanation = ''; let newMarkdown = currentContent; // Default, keep old content - if (parts.length >= 2) { - explanation = parts[0].trim(); - newMarkdown = parts[1].trim(); + if (commentMatch && commentMatch[1]) { + explanation = commentMatch[1].trim(); + } + + if (documentMatch && documentMatch[1]) { + newMarkdown = documentMatch[1].trim(); // Update for next iteration currentContent = newMarkdown; - } else { - // Fallback if no split found - explanation = result; + } + + // Fallback: if no code blocks found, try old format for compatibility + if (!commentMatch && !documentMatch) { + const parts = result.split('---SPLIT---'); + if (parts.length >= 2) { + explanation = parts[0].trim(); + newMarkdown = parts[1].trim(); + currentContent = newMarkdown; + } else { + explanation = result; + } } // Send this iteration's response