Muyue 89c10b14a7 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)
2025-10-14 20:28:32 +02:00

460 lines
13 KiB
JavaScript

const express = require('express');
const router = express.Router();
const https = require('https');
require('dotenv').config({ path: './config/.env' });
// Mistral AI Configuration
const MISTRAL_API_KEY = process.env.MISTRAL_API_KEY;
const MISTRAL_BASE_URL = process.env.MISTRAL_BASE_URL || 'https://api.mistral.ai/v1';
const MISTRAL_MODEL = process.env.MISTRAL_MODEL || 'mistral-large-latest';
const AI_ENABLED = process.env.AI_ENABLED === 'true';
// Verification middleware
function checkAIEnabled(req, res, next) {
if (!AI_ENABLED) {
return res.status(503).json({
success: false,
error: 'AI features are disabled'
});
}
if (!MISTRAL_API_KEY) {
return res.status(500).json({
success: false,
error: 'Mistral API key not configured'
});
}
next();
}
// 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',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${MISTRAL_API_KEY}`
},
body: JSON.stringify({
model: MISTRAL_MODEL,
messages: messages,
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}`);
}
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;
}
}
// POST /api/ai/rephrase - Rephrase text
router.post('/rephrase', checkAIEnabled, async (req, res) => {
try {
const { text, context = '' } = req.body;
if (!text || text.trim().length === 0) {
return res.status(400).json({
success: false,
error: 'Text to rephrase is required'
});
}
const messages = [
{
role: 'system',
content: `You are an assistant specialized in rephrasing technical and design texts.
STRICT RULES:
1. Rephrase the text to improve clarity, style, and fluency
2. Preserve the technical level and all important details
3. Respond ONLY with the final rephrased text
4. No introduction, conclusion, explanation, or commentary
5. Do not start with "Here is", "The rephrased text" or other preamble
6. Start directly with the rephrased content
${context ? `Document context: ${context}` : ''}`
},
{
role: 'user',
content: `Rephrase this text: "${text}"`
}
];
const result = await callMistralAPI(messages, 0.2); // Precise rephrasing
res.json({
success: true,
data: {
original: text,
rephrased: result
}
});
} catch (error) {
console.error('Rephrasing error:', error);
res.status(500).json({
success: false,
error: 'Error during rephrasing: ' + error.message
});
}
});
// POST /api/ai/check-inconsistencies - Check for inconsistencies
router.post('/check-inconsistencies', checkAIEnabled, async (req, res) => {
try {
const { content } = req.body;
if (!content || content.trim().length === 0) {
return res.status(400).json({
success: false,
error: 'Content to analyze is required'
});
}
const messages = [
{
role: 'system',
content: `You are an expert in technical design document analysis.
Analyze the following document and identify all potential inconsistencies:
- Contradictions in information
- Conflicting decisions
- Inconsistencies in terminology
- Logical issues in architecture or choices
Respond directly with your detailed analysis of the inconsistencies found.`
},
{
role: 'user',
content: `Analyze this document to detect inconsistencies:\n\n${content}`
}
];
const result = await callMistralAPI(messages, 0.1); // Precise and factual analysis
res.json({
success: true,
data: {
analysis: result
}
});
} catch (error) {
console.error('Inconsistencies analysis error:', error);
res.status(500).json({
success: false,
error: 'Error during analysis: ' + error.message
});
}
});
// POST /api/ai/check-duplications - Check for duplications
router.post('/check-duplications', checkAIEnabled, async (req, res) => {
try {
const { content } = req.body;
if (!content || content.trim().length === 0) {
return res.status(400).json({
success: false,
error: 'Content to analyze is required'
});
}
const messages = [
{
role: 'system',
content: `You are a content analysis expert.
Analyze the following document to identify:
- Repeated or redundant information
- Sections that cover the same topic
- Duplicated explanations
- Concepts presented multiple times
Suggest ways to eliminate these duplications while preserving important information.
Respond directly with your analysis and suggestions.`
},
{
role: 'user',
content: `Analyze this document to detect duplications:\n\n${content}`
}
];
const result = await callMistralAPI(messages, 0.1); // Precise and factual analysis
res.json({
success: true,
data: {
analysis: result
}
});
} catch (error) {
console.error('Duplications analysis error:', error);
res.status(500).json({
success: false,
error: 'Error during analysis: ' + error.message
});
}
});
// POST /api/ai/give-advice - Give advice
router.post('/give-advice', checkAIEnabled, async (req, res) => {
try {
const { content, domain = 'general' } = req.body;
if (!content || content.trim().length === 0) {
return res.status(400).json({
success: false,
error: 'Content to analyze is required'
});
}
const messages = [
{
role: 'system',
content: `You are an expert consultant in design and technical architecture in the domain: ${domain}.
Analyze the provided design document and give constructive advice to improve it.
Focus on:
- Documentation completeness
- Clarity of explanations
- Content organization
- Domain best practices
- Important missing points
- Concrete improvement suggestions
Be constructive and practical in your recommendations.
Respond directly with your advice and improvement suggestions.`
},
{
role: 'user',
content: `Analyze this design document and provide advice to improve it:\n\n${content}`
}
];
const result = await callMistralAPI(messages, 0.4); // Balanced advice
res.json({
success: true,
data: {
advice: result
}
});
} catch (error) {
console.error('Advice error:', error);
res.status(500).json({
success: false,
error: 'Error during analysis: ' + error.message
});
}
});
// POST /api/ai/liberty-mode - Enhanced mode (iterative generation)
router.post('/liberty-mode', checkAIEnabled, async (req, res) => {
try {
const { content, iterations = 1, precision = 70, focus = 'design' } = req.body;
if (!content || content.trim().length === 0) {
return res.status(400).json({
success: false,
error: 'Base content is required'
});
}
const maxIterations = Math.min(parseInt(iterations), 10); // Limit to 10 iterations
const precisionPercent = Math.min(Math.max(parseInt(precision), 10), 100); // Between 10% and 100%
// Configure streaming for real-time responses
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*'
});
let currentContent = content;
for (let i = 0; i < maxIterations; i++) {
try {
const messages = [
{
role: 'system',
content: `You are a technical design expert with "Enhanced Mode".
MISSION: Improve and enrich the document respecting EXACTLY this precision level: ${precisionPercent}%
PRECISION RULES:
- At ${precisionPercent}%: You can deduce and add content up to ${precisionPercent}% based on existing information
- At ${100 - precisionPercent}%: You can create logical and relevant content even without explicit info in the text
INSTRUCTIONS:
1. Enrich ALL of the document consistently
2. Add sections, details, explanations, conceptual diagrams
3. Develop existing ideas with the allowed creativity
4. Maintain logical structure
MANDATORY RESPONSE FORMAT - Use code blocks with triple backticks:
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
Then, write the complete improved markdown document inside a code block labeled "document".
Example format:
First block: comment${i + 1}
Second block: document
Focus: ${focus}
Precision: ${precisionPercent}%
Iteration: ${i + 1}/${maxIterations}`
},
{
role: 'user',
content: `Document to improve (Iteration ${i + 1}):\n\n${currentContent}`
}
];
// Temperature based on precision (more creative = higher temperature)
const temperature = (100 - precisionPercent) / 100 * 0.8 + 0.1; // Between 0.1 and 0.9
const result = await callMistralAPI(messages, temperature);
// 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();
currentContent = newMarkdown;
} else {
explanation = result;
}
}
// Send this iteration's response
const iterationData = {
iteration: i + 1,
explanation: explanation,
markdown: newMarkdown,
completed: false
};
res.write(`data: ${JSON.stringify(iterationData)}\n\n`);
// Small delay to allow client-side display
await new Promise(resolve => setTimeout(resolve, 500));
} catch (iterationError) {
console.error(`Iteration ${i + 1} error:`, iterationError);
const errorData = {
iteration: i + 1,
error: `Iteration ${i + 1} error: ${iterationError.message}`,
completed: true
};
res.write(`data: ${JSON.stringify(errorData)}\n\n`);
break;
}
}
// End signal
const finalData = {
completed: true,
totalIterations: maxIterations,
finalMarkdown: currentContent
};
res.write(`data: ${JSON.stringify(finalData)}\n\n`);
res.end();
} catch (error) {
console.error('Enhanced mode error:', error);
const errorData = {
error: 'Error during generation: ' + error.message,
completed: true
};
res.write(`data: ${JSON.stringify(errorData)}\n\n`);
res.end();
}
});
// GET /api/ai/status - AI status
router.get('/status', (req, res) => {
res.json({
success: true,
data: {
enabled: AI_ENABLED,
model: MISTRAL_MODEL,
configured: !!MISTRAL_API_KEY
}
});
});
module.exports = router;