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-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" */
|
||||
|
||||
126
assets/js/app.js
126
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 = '<ul>';
|
||||
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 += '<ul>';
|
||||
}
|
||||
} else if (item.level < currentLevel) {
|
||||
for (let i = item.level; i < currentLevel; i++) {
|
||||
tocHtml += '</ul>';
|
||||
}
|
||||
// Fermer les listes nécessaires
|
||||
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>`;
|
||||
currentLevel = item.level;
|
||||
// Ajouter l'élément avec indentation CSS
|
||||
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 {
|
||||
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) {
|
||||
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 = '';
|
||||
|
||||
73
routes/ai.js
73
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,21 +363,37 @@ 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 (commentMatch && commentMatch[1]) {
|
||||
explanation = commentMatch[1].trim();
|
||||
}
|
||||
|
||||
if (documentMatch && documentMatch[1]) {
|
||||
newMarkdown = documentMatch[1].trim();
|
||||
// Update for next iteration
|
||||
currentContent = newMarkdown;
|
||||
}
|
||||
|
||||
// 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();
|
||||
// Update for next iteration
|
||||
currentContent = newMarkdown;
|
||||
} else {
|
||||
// Fallback if no split found
|
||||
explanation = result;
|
||||
}
|
||||
}
|
||||
|
||||
// Send this iteration's response
|
||||
const iterationData = {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user