fix: Amélioration navigation TOC, preview et API Mistral
- TOC: Ajout indentation par niveau et correction des puces sur h6+ - TOC: Correction navigation avec scroll précis (TreeWalker) - TOC: Séparation scroll page/TOC avec position sticky - Preview: Correction bug ** apparaissant au retour en mode édition - Enhanced Mode: Nouveau format de réponse (blocs comment/document) - API: Ajout timeouts 120s et agent HTTPS pour éviter erreurs réseau - CSS: Ajout overflow-y sur sections pour scroll indépendant Fichiers modifiés : - assets/js/app.js (TOC, navigation, preview) - assets/css/style.css (scroll, sticky positioning) - routes/ai.js (timeouts, format réponse)
This commit is contained in:
parent
daef2bbf1a
commit
89c10b14a7
@ -590,7 +590,11 @@ section h2 {
|
|||||||
|
|
||||||
/* Table des matières */
|
/* Table des matières */
|
||||||
#table-of-contents {
|
#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 {
|
#toc-nav {
|
||||||
@ -599,10 +603,12 @@ section h2 {
|
|||||||
|
|
||||||
#toc-nav ul {
|
#toc-nav ul {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
padding-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#toc-nav li {
|
#toc-nav li {
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#toc-nav a {
|
#toc-nav a {
|
||||||
@ -621,8 +627,9 @@ section h2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#toc-nav ul ul {
|
#toc-nav ul ul {
|
||||||
margin-left: 1rem;
|
margin-left: 0;
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
|
padding-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#toc-nav ul ul a {
|
#toc-nav ul ul a {
|
||||||
@ -633,6 +640,8 @@ section h2 {
|
|||||||
/* Zone d'écriture */
|
/* Zone d'écriture */
|
||||||
#design-journal {
|
#design-journal {
|
||||||
min-height: 600px;
|
min-height: 600px;
|
||||||
|
max-height: calc(100vh - 200px);
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
#journal-editor {
|
#journal-editor {
|
||||||
@ -644,7 +653,6 @@ section h2 {
|
|||||||
border: none;
|
border: none;
|
||||||
background: var(--surface-color);
|
background: var(--surface-color);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
resize: vertical;
|
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
@ -693,7 +701,10 @@ section h2 {
|
|||||||
#ai-assistant {
|
#ai-assistant {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
/* Comportement normal de scroll */
|
max-height: calc(100vh - 200px);
|
||||||
|
position: sticky;
|
||||||
|
top: 100px;
|
||||||
|
align-self: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Styles pour "Coming Soon" */
|
/* Styles pour "Coming Soon" */
|
||||||
|
|||||||
126
assets/js/app.js
126
assets/js/app.js
@ -434,7 +434,6 @@ class ConceptionAssistant {
|
|||||||
const content = this.editor.innerText;
|
const content = this.editor.innerText;
|
||||||
const lines = content.split('\n');
|
const lines = content.split('\n');
|
||||||
const toc = [];
|
const toc = [];
|
||||||
let tocHtml = '';
|
|
||||||
|
|
||||||
// Remove existing anchors
|
// Remove existing anchors
|
||||||
const existingAnchors = this.editor.querySelectorAll('.heading-anchor');
|
const existingAnchors = this.editor.querySelectorAll('.heading-anchor');
|
||||||
@ -451,29 +450,42 @@ class ConceptionAssistant {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temporarily disabled to avoid disrupting typing
|
let tocHtml = '';
|
||||||
// this.addHeadingAnchors(lines, toc);
|
|
||||||
|
|
||||||
if (toc.length > 0) {
|
if (toc.length > 0) {
|
||||||
tocHtml = '<ul>';
|
// Nouvelle génération de TOC avec indentation correcte
|
||||||
let currentLevel = 0;
|
let previousLevel = 0;
|
||||||
|
|
||||||
for (const item of toc) {
|
for (let i = 0; i < toc.length; i++) {
|
||||||
if (item.level > currentLevel) {
|
const item = toc[i];
|
||||||
for (let i = currentLevel; i < item.level - 1; i++) {
|
|
||||||
|
// Ouvrir les listes nécessaires
|
||||||
|
if (item.level > previousLevel) {
|
||||||
|
for (let j = previousLevel; j < item.level; j++) {
|
||||||
tocHtml += '<ul>';
|
tocHtml += '<ul>';
|
||||||
}
|
}
|
||||||
} else if (item.level < currentLevel) {
|
}
|
||||||
for (let i = item.level; i < currentLevel; i++) {
|
// Fermer les listes nécessaires
|
||||||
tocHtml += '</ul>';
|
else if (item.level < previousLevel) {
|
||||||
|
for (let j = item.level; j < previousLevel; j++) {
|
||||||
|
tocHtml += '</ul></li>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Même niveau, fermer le li précédent
|
||||||
|
else if (i > 0) {
|
||||||
|
tocHtml += '</li>';
|
||||||
|
}
|
||||||
|
|
||||||
tocHtml += `<li><a href="#${item.id}" onclick="app.scrollToHeading('${item.title}'); return false;">${item.title}</a></li>`;
|
// Ajouter l'élément avec indentation CSS
|
||||||
currentLevel = item.level;
|
const indent = (item.level - 1) * 1; // 1rem par niveau
|
||||||
|
tocHtml += `<li style="margin-left: ${indent}rem;"><a href="#${item.id}" onclick="app.scrollToHeading('${item.title.replace(/'/g, "\\'")}'); return false;">${item.title}</a>`;
|
||||||
|
|
||||||
|
previousLevel = item.level;
|
||||||
}
|
}
|
||||||
|
|
||||||
tocHtml += '</ul>';
|
// Fermer toutes les listes restantes
|
||||||
|
for (let j = 0; j < previousLevel; j++) {
|
||||||
|
tocHtml += '</li></ul>';
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
tocHtml = '<div class="toc-placeholder"><p>Add headings (# ## ###) to your journal to generate the table of contents.</p></div>';
|
tocHtml = '<div class="toc-placeholder"><p>Add headings (# ## ###) to your journal to generate the table of contents.</p></div>';
|
||||||
}
|
}
|
||||||
@ -505,18 +517,21 @@ class ConceptionAssistant {
|
|||||||
|
|
||||||
scrollToHeading(title) {
|
scrollToHeading(title) {
|
||||||
try {
|
try {
|
||||||
// Simpler and more robust method: search text directly in editor
|
|
||||||
const content = this.editor.innerText;
|
const content = this.editor.innerText;
|
||||||
const lines = content.split('\n');
|
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 targetLineIndex = -1;
|
||||||
|
let charPosition = 0;
|
||||||
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
for (let i = 0; i < lines.length; i++) {
|
||||||
const line = lines[i].trim();
|
const line = lines[i].trim();
|
||||||
if (line.startsWith('#') && line.replace(/^#+\s*/, '') === title) {
|
if (line.startsWith('#') && line.replace(/^#+\s*/, '') === title) {
|
||||||
targetLineIndex = i;
|
targetLineIndex = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
// Count characters including newlines
|
||||||
|
charPosition += lines[i].length + 1; // +1 for \n
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetLineIndex === -1) {
|
if (targetLineIndex === -1) {
|
||||||
@ -524,37 +539,56 @@ class ConceptionAssistant {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate approximate line position
|
// Create a range at the target position to get accurate coordinates
|
||||||
const editorStyles = window.getComputedStyle(this.editor);
|
const selection = window.getSelection();
|
||||||
const lineHeight = parseFloat(editorStyles.lineHeight) || 20;
|
const range = document.createRange();
|
||||||
const paddingTop = parseFloat(editorStyles.paddingTop) || 0;
|
|
||||||
|
|
||||||
// Calculate scroll position based on line number
|
// Find the text node at the character position
|
||||||
const targetScrollPosition = (targetLineIndex * lineHeight) + paddingTop;
|
const walker = document.createTreeWalker(
|
||||||
|
this.editor,
|
||||||
|
NodeFilter.SHOW_TEXT,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
// Determine if editor or window should be scrolled
|
let currentChar = 0;
|
||||||
const editorRect = this.editor.getBoundingClientRect();
|
let targetNode = null;
|
||||||
const editorHasScroll = this.editor.scrollHeight > this.editor.clientHeight;
|
let offsetInNode = 0;
|
||||||
|
|
||||||
if (editorHasScroll) {
|
while (walker.nextNode()) {
|
||||||
// Scroll within editor
|
const node = walker.currentNode;
|
||||||
this.editor.scrollTo({
|
const nodeLength = node.textContent.length;
|
||||||
top: Math.max(0, targetScrollPosition - 60),
|
|
||||||
behavior: 'smooth'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Scroll entire page
|
|
||||||
const editorTop = this.editor.offsetTop;
|
|
||||||
const windowScrollTarget = editorTop + targetScrollPosition - 100;
|
|
||||||
|
|
||||||
window.scrollTo({
|
if (currentChar + nodeLength >= charPosition) {
|
||||||
top: Math.max(0, windowScrollTarget),
|
targetNode = node;
|
||||||
behavior: 'smooth'
|
offsetInNode = charPosition - currentChar;
|
||||||
});
|
break;
|
||||||
|
}
|
||||||
|
currentChar += nodeLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional: temporarily highlight the title
|
if (targetNode) {
|
||||||
this.highlightHeading(title);
|
// 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');
|
this.showNotification(`Navigating to: ${title}`, 'success');
|
||||||
|
|
||||||
@ -1297,8 +1331,8 @@ class ConceptionAssistant {
|
|||||||
const mainElement = document.querySelector('main');
|
const mainElement = document.querySelector('main');
|
||||||
|
|
||||||
if (!this.isPreviewMode) {
|
if (!this.isPreviewMode) {
|
||||||
// Switch to preview mode
|
// Switch to preview mode - Save innerText instead of innerHTML
|
||||||
this.originalContent = this.editor.innerHTML;
|
this.originalContent = this.editor.innerText;
|
||||||
const markdownContent = this.editor.innerText;
|
const markdownContent = this.editor.innerText;
|
||||||
|
|
||||||
// Add preview-mode class to main to adjust grid layout
|
// Add preview-mode class to main to adjust grid layout
|
||||||
@ -1392,9 +1426,9 @@ class ConceptionAssistant {
|
|||||||
this.showNotification('Preview mode activated', 'success');
|
this.showNotification('Preview mode activated', 'success');
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Return to edit mode
|
// Return to edit mode - Restore innerText instead of innerHTML
|
||||||
this.editor.contentEditable = true;
|
this.editor.contentEditable = true;
|
||||||
this.editor.innerHTML = this.originalContent;
|
this.editor.innerText = this.originalContent;
|
||||||
this.editor.style.background = '';
|
this.editor.style.background = '';
|
||||||
this.editor.style.border = '';
|
this.editor.style.border = '';
|
||||||
this.editor.style.borderRadius = '';
|
this.editor.style.borderRadius = '';
|
||||||
|
|||||||
81
routes/ai.js
81
routes/ai.js
@ -1,5 +1,6 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
const https = require('https');
|
||||||
require('dotenv').config({ path: './config/.env' });
|
require('dotenv').config({ path: './config/.env' });
|
||||||
|
|
||||||
// Mistral AI Configuration
|
// Mistral AI Configuration
|
||||||
@ -27,8 +28,18 @@ function checkAIEnabled(req, res, next) {
|
|||||||
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) {
|
async function callMistralAPI(messages, temperature = null) {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), 120000); // 120 seconds timeout
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${MISTRAL_BASE_URL}/chat/completions`, {
|
const response = await fetch(`${MISTRAL_BASE_URL}/chat/completions`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -42,9 +53,13 @@ async function callMistralAPI(messages, temperature = null) {
|
|||||||
temperature: temperature !== null ? temperature : parseFloat(process.env.AI_TEMPERATURE) || 0.3,
|
temperature: temperature !== null ? temperature : parseFloat(process.env.AI_TEMPERATURE) || 0.3,
|
||||||
max_tokens: parseInt(process.env.AI_MAX_TOKENS) || 35000,
|
max_tokens: parseInt(process.env.AI_MAX_TOKENS) || 35000,
|
||||||
top_p: parseFloat(process.env.AI_TOP_P) || 0.85
|
top_p: parseFloat(process.env.AI_TOP_P) || 0.85
|
||||||
})
|
}),
|
||||||
|
signal: controller.signal,
|
||||||
|
agent: httpsAgent
|
||||||
});
|
});
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.text();
|
const error = await response.text();
|
||||||
throw new Error(`Mistral API Error: ${response.status} - ${error}`);
|
throw new Error(`Mistral API Error: ${response.status} - ${error}`);
|
||||||
@ -53,7 +68,20 @@ async function callMistralAPI(messages, temperature = null) {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data.choices[0].message.content;
|
return data.choices[0].message.content;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
console.error('Mistral API Error:', error);
|
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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -307,17 +335,22 @@ router.post('/liberty-mode', checkAIEnabled, async (req, res) => {
|
|||||||
3. Develop existing ideas with the allowed creativity
|
3. Develop existing ideas with the allowed creativity
|
||||||
4. Maintain logical structure
|
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})
|
First, write your comment explaining the improvements inside a code block labeled "comment${i + 1}":
|
||||||
[Explain the improvements made, sections added, reasoning]
|
- 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}
|
Focus: ${focus}
|
||||||
Precision: ${precisionPercent}%`
|
Precision: ${precisionPercent}%
|
||||||
|
Iteration: ${i + 1}/${maxIterations}`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
role: 'user',
|
role: 'user',
|
||||||
@ -330,20 +363,36 @@ router.post('/liberty-mode', checkAIEnabled, async (req, res) => {
|
|||||||
|
|
||||||
const result = await callMistralAPI(messages, temperature);
|
const result = await callMistralAPI(messages, temperature);
|
||||||
|
|
||||||
// Separate explanation from markdown
|
// Extract code blocks
|
||||||
const parts = result.split('---SPLIT---');
|
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 explanation = '';
|
||||||
let newMarkdown = currentContent; // Default, keep old content
|
let newMarkdown = currentContent; // Default, keep old content
|
||||||
|
|
||||||
if (parts.length >= 2) {
|
if (commentMatch && commentMatch[1]) {
|
||||||
explanation = parts[0].trim();
|
explanation = commentMatch[1].trim();
|
||||||
newMarkdown = parts[1].trim();
|
}
|
||||||
|
|
||||||
|
if (documentMatch && documentMatch[1]) {
|
||||||
|
newMarkdown = documentMatch[1].trim();
|
||||||
// Update for next iteration
|
// Update for next iteration
|
||||||
currentContent = newMarkdown;
|
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
|
// Send this iteration's response
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user