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 = '
';
- let currentLevel = 0;
+ // Nouvelle génération de TOC avec indentation correcte
+ let previousLevel = 0;
- for (const item of toc) {
- if (item.level > currentLevel) {
- for (let i = currentLevel; i < item.level - 1; i++) {
+ 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 += '';
}
- } else if (item.level < currentLevel) {
- for (let i = item.level; i < currentLevel; i++) {
- 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 += '';
+ }
- 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