Muyue 86eb68c0e6 feat: Enhanced mode v2 with Smart Adaptive strategy, comprehensive logging, and internationalization
Major Features:
  - Enhanced Mode v2: Smart Adaptive section selection strategy
    * AI automatically selects ONE optimal section per iteration
    * Intelligent selection based on quality assessment and strategic importance
    * Section tracking to avoid duplicate modifications
    * Fixed header level preservation (## stays ##, ### stays ###)
    * Updated document code block format (document)

  - Mermaid Auto-Fix System
    * New POST /api/ai/fix-mermaid endpoint for automatic diagram correction
    * Automatic error detection in preview and presentation modes
    * Inter-window communication for seamless editor updates
    * AI-powered syntax error resolution

  - Comprehensive Logging System
    * Centralized logger utility (utils/logger.js)
    * API request/response middleware logging
    * AI operations: rephrase, check-inconsistencies, check-duplications, give-advice, liberty-mode, fix-mermaid
    * Storage operations: create, read, update, delete journals
    * Export operations: PDF and Web ZIP generation
    * Structured logging with timestamps, levels, categories, and metadata

  Improvements:
  - Table of Contents: Clean display with markdown formatting stripped from titles
  - Section badges: Fixed visibility with hardcoded blue background (#2563eb)
  - Internationalization: Complete English translation (lang, UI text, placeholders, comments)
  - Code cleanup: Removed all emojis from codebase

  Technical Changes:
  - routes/ai.js: Enhanced mode implementation, Mermaid fix endpoint, comprehensive logging
  - routes/api.js: Storage operation logging
  - routes/export.js: Export operation logging
  - routes/index.js: Presentation mode Mermaid auto-fix
  - assets/js/app.js: TOC markdown stripping, Mermaid error handling, message listeners
  - assets/css/style.css: Dark mode comment, English placeholders, header icon
  - views/page.js: English lang attribute, favicon, scroll-to-top button
  - views/header.js: Theme toggle icon
  - utils/logger.js: New centralized logging system
  - app.js: API request/response logging middleware
2025-10-15 10:21:54 +02:00

686 lines
17 KiB
JavaScript

const express = require('express');
const router = express.Router();
const puppeteer = require('puppeteer');
const path = require('path');
const archiver = require('archiver');
const fs = require('fs');
const { logger } = require('../utils/logger');
// POST /api/export/pdf - Generate PDF from markdown content
router.post('/pdf', async (req, res) => {
const startTime = Date.now();
const { content, title = 'Design Journal' } = req.body;
logger.exportOperation('pdf', 'application/pdf', content.length);
if (!content || content.trim() === '') {
logger.error('EXPORT', 'PDF export failed: Content required');
return res.status(400).json({
success: false,
error: 'Content required to generate PDF'
});
}
let browser;
try {
// Convert markdown to HTML with styles
const htmlContent = generateStyledHTML(content, title);
// Launch Puppeteer
browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const page = await browser.newPage();
// Set HTML content
await page.setContent(htmlContent, {
waitUntil: 'domcontentloaded',
timeout: parseInt(process.env.PDF_TIMEOUT) || 30000
});
// Wait for Mermaid to load and diagrams to render
await new Promise(resolve => setTimeout(resolve, 2000));
// Check if Mermaid diagrams exist and wait for them to load
try {
await page.waitForFunction(() => {
const mermaidElements = document.querySelectorAll('.mermaid');
if (mermaidElements.length === 0) return true;
return Array.from(mermaidElements).every(el =>
el.querySelector('svg') || el.textContent.trim() === '' || el.classList.contains('error')
);
}, { timeout: 10000 });
} catch (e) {
console.log('Timeout waiting for Mermaid, proceeding...');
}
// Generate PDF
const pdfBuffer = await page.pdf({
format: 'A4',
margin: {
top: '2cm',
right: '2cm',
bottom: '2cm',
left: '2cm'
},
printBackground: true,
displayHeaderFooter: true,
headerTemplate: `
<div style="font-size: 10px; width: 100%; text-align: center; color: #666; margin-top: 1cm;">
<span>${title}</span>
</div>
`,
footerTemplate: `
<div style="font-size: 10px; width: 100%; text-align: center; color: #666; margin-bottom: 1cm;">
<span>Page <span class="pageNumber"></span> of <span class="totalPages"></span> - Generated on ${new Date().toLocaleDateString('en-US')}</span>
</div>
`
});
await browser.close();
// Check PDF size
const maxSize = parseInt(process.env.PDF_MAX_SIZE) || 10485760; // 10MB default
if (pdfBuffer.length > maxSize) {
return res.status(413).json({
success: false,
error: 'Generated PDF exceeds maximum allowed size'
});
}
// Send PDF
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `attachment; filename="${sanitizeFilename(title)}.pdf"`);
res.setHeader('Content-Length', pdfBuffer.length);
logger.info('EXPORT', `PDF generated successfully in ${Date.now() - startTime}ms`, { size: pdfBuffer.length, title });
res.send(pdfBuffer);
} catch (error) {
logger.error('EXPORT', 'PDF generation error', { error: error.message, title });
if (browser) {
await browser.close();
}
res.status(500).json({
success: false,
error: 'Error during PDF generation'
});
}
});
// Utility function to escape HTML
function escapeHtml(text) {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, function(m) { return map[m]; });
}
// Function to convert markdown to styled HTML with Mermaid support
function generateStyledHTML(content, title) {
let html = content;
// Process Mermaid diagrams BEFORE code blocks
html = html.replace(/```mermaid\n([\s\S]*?)```/g, '<div class="mermaid">$1</div>');
// Process code blocks with language
html = html.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => {
const language = lang ? ` class="language-${lang}"` : '';
return `<pre><code${language}>${escapeHtml(code)}</code></pre>`;
});
// Inline code
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
// Enhanced table processing
html = html.replace(/(\|.*\|\s*\n)+/g, (match) => {
const lines = match.trim().split('\n').filter(line => line.trim());
let tableHtml = '<table>\n';
let inHeader = true;
lines.forEach((line, index) => {
// Ignore separator lines
if (line.match(/^\s*\|[\s\-:]*\|\s*$/)) {
inHeader = false;
return;
}
const cells = line.split('|')
.map(cell => cell.trim())
.filter((cell, idx, arr) => idx > 0 && idx < arr.length - 1);
if (cells.length === 0) return;
const tag = inHeader ? 'th' : 'td';
tableHtml += ' <tr>\n';
cells.forEach(cell => {
tableHtml += ` <${tag}>${cell}</${tag}>\n`;
});
tableHtml += ' </tr>\n';
if (inHeader && index === 0) inHeader = false;
});
tableHtml += '</table>\n';
return tableHtml;
});
// Markdown to HTML conversion
html = html
// Headings (in descending order)
.replace(/^#{6}\s+(.*$)/gm, '<h6>$1</h6>')
.replace(/^#{5}\s+(.*$)/gm, '<h5>$1</h5>')
.replace(/^#{4}\s+(.*$)/gm, '<h4>$1</h4>')
.replace(/^#{3}\s+(.*$)/gm, '<h3>$1</h3>')
.replace(/^#{2}\s+(.*$)/gm, '<h2>$1</h2>')
.replace(/^#{1}\s+(.*$)/gm, '<h1>$1</h1>')
// Blockquotes
.replace(/^>\s+(.*$)/gm, '<blockquote>$1</blockquote>')
// Bold and italic (order matters)
.replace(/\*\*\*(.*?)\*\*\*/g, '<strong><em>$1</em></strong>')
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
// Strikethrough
.replace(/~~(.*?)~~/g, '<del>$1</del>')
// Numbered lists
.replace(/^(\s*)(\d+)\. (.*$)/gm, '<li class="numbered">$3</li>')
// Bullet lists and tasks
.replace(/^(\s*)- \[x\] (.*$)/gm, '<li class="todo-done">$2</li>')
.replace(/^(\s*)- \[ \] (.*$)/gm, '<li class="todo">$2</li>')
.replace(/^(\s*)[*\-+] (.*$)/gm, '<li>$2</li>')
// Links
.replace(/\[([^\]]+)\]\(([^\)]+)\)/g, '<a href="$2" target="_blank">$1</a>')
// Horizontal rule
.replace(/^---+$/gm, '<hr>');
// Group consecutive lists
html = html.replace(/(<li[^>]*>.*?<\/li>\s*)+/gs, (match) => {
if (match.includes('class="numbered"')) {
return '<ol>' + match.replace(/ class="numbered"/g, '') + '</ol>';
} else {
return '<ul>' + match + '</ul>';
}
});
// Clean up consecutive multiple lists
html = html.replace(/(<\/[uo]l>\s*<[uo]l>)/g, '');
// Group consecutive blockquotes
html = html.replace(/(<blockquote>.*?<\/blockquote>\s*)+/gs, (match) => {
const content = match.replace(/<\/?blockquote>/g, '');
return '<blockquote>' + content + '</blockquote>';
});
// Process paragraphs
html = html.split('\n\n').map(paragraph => {
paragraph = paragraph.trim();
if (!paragraph) return '';
// Don't wrap block elements in paragraphs
if (paragraph.match(/^<(h[1-6]|div|table|ul|ol|blockquote|hr|pre)/)) {
return paragraph;
}
return '<p>' + paragraph.replace(/\n/g, '<br>') + '</p>';
}).join('\n\n');
const styledHTML = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${title}</title>
<script src="https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js"></script>
<script>
mermaid.initialize({
startOnLoad: true,
theme: 'default',
themeVariables: {
primaryColor: '#0969da',
primaryTextColor: '#24292f',
primaryBorderColor: '#0969da',
lineColor: '#656d76',
sectionBkgColor: '#f6f8fa',
altSectionBkgColor: '#ffffff',
gridColor: '#d1d9e0',
secondaryColor: '#f6f8fa',
tertiaryColor: '#0969da'
},
fontFamily: '-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif',
fontSize: 14
});
</script>
<style>
@page {
size: A4;
margin: 2cm;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Helvetica, Roboto, Arial, sans-serif;
line-height: 1.6;
color: #24292f;
font-size: 11pt;
background: white;
word-wrap: break-word;
}
/* GitHub-style headings */
h1 {
color: #24292f;
font-size: 2em;
font-weight: 600;
margin: 0 0 16pt 0;
padding-bottom: 8pt;
border-bottom: 1px solid #d1d9e0;
page-break-after: avoid;
}
h2 {
color: #24292f;
font-size: 1.5em;
font-weight: 600;
margin: 24pt 0 12pt 0;
padding-bottom: 6pt;
border-bottom: 1px solid #d1d9e0;
page-break-after: avoid;
}
h3 {
color: #24292f;
font-size: 1.25em;
font-weight: 600;
margin: 18pt 0 8pt 0;
page-break-after: avoid;
}
h4 {
color: #24292f;
font-size: 1em;
font-weight: 600;
margin: 16pt 0 6pt 0;
page-break-after: avoid;
}
h5, h6 {
color: #24292f;
font-size: 0.875em;
font-weight: 600;
margin: 14pt 0 6pt 0;
page-break-after: avoid;
}
/* Paragraphs */
p {
margin: 0 0 12pt 0;
orphans: 2;
widows: 2;
}
/* Lists */
ul, ol {
margin: 0 0 12pt 0;
padding-left: 2em;
}
ul {
list-style-type: disc;
}
ol {
list-style-type: decimal;
}
li {
margin: 0 0 4pt 0;
line-height: 1.5;
}
/* Tasks */
li.todo {
list-style: none;
margin-left: -2em;
padding-left: 1.5em;
}
li.todo-done {
list-style: none;
margin-left: -2em;
padding-left: 1.5em;
color: #656d76;
}
/* Text formatting */
strong {
font-weight: 600;
}
em {
font-style: italic;
}
del {
text-decoration: line-through;
color: #656d76;
}
/* Links */
a {
color: #0969da;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
/* Blockquotes */
blockquote {
margin: 12pt 0;
padding: 0 12pt;
border-left: 4px solid #d1d9e0;
color: #656d76;
}
blockquote > :first-child {
margin-top: 0;
}
blockquote > :last-child {
margin-bottom: 0;
}
/* Horizontal rule */
hr {
margin: 24pt 0;
border: none;
border-top: 1px solid #d1d9e0;
height: 0;
}
/* GitHub-style tables */
table {
border-collapse: collapse;
width: 100%;
margin: 12pt 0;
font-size: 10.5pt;
border: 1px solid #d1d9e0;
}
th, td {
border: 1px solid #d1d9e0;
padding: 6pt 10pt;
text-align: left;
}
th {
background-color: #f6f8fa;
font-weight: 600;
}
tr:nth-child(even) {
background-color: #f6f8fa;
}
/* GitHub-style code */
code {
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
background-color: rgba(175, 184, 193, 0.2);
padding: 1pt 4pt;
border-radius: 3pt;
font-size: 85%;
}
pre {
background-color: #f6f8fa;
padding: 12pt;
border-radius: 6pt;
border: 1px solid #d1d9e0;
margin: 12pt 0;
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
font-size: 85%;
line-height: 1.45;
overflow: visible;
}
pre code {
background: transparent;
padding: 0;
border-radius: 0;
font-size: inherit;
}
/* Mermaid diagrams */
.mermaid {
text-align: center;
margin: 16pt 0;
page-break-inside: avoid;
}
.mermaid svg {
max-width: 100%;
height: auto;
}
/* Avoid page breaks */
h1, h2, h3, h4, h5, h6 {
page-break-after: avoid;
}
p, li {
page-break-inside: avoid;
orphans: 2;
widows: 2;
}
pre, table {
page-break-inside: avoid;
}
/* Improve readability */
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
</style>
</head>
<body>
${html}
</body>
</html>
`;
return styledHTML;
}
// Function to sanitize filename
function sanitizeFilename(filename) {
return filename
.replace(/[^\w\s-]/g, '') // Remove special characters
.replace(/\s+/g, '-') // Replace spaces with dashes
.toLowerCase() // Lowercase
.substring(0, 100); // Limit length
}
// POST /api/export/web - Generate ZIP with HTML + CSS
router.post('/web', async (req, res) => {
const startTime = Date.now();
const { content, title = 'Design Journal' } = req.body;
logger.exportOperation('web-zip', 'application/zip', content.length);
if (!content || content.trim() === '') {
logger.error('EXPORT', 'Web export failed: Content required');
return res.status(400).json({
success: false,
error: 'Content required to generate web export'
});
}
try {
// Read CSS files
const styleCssPath = path.join(__dirname, '../assets/css/style.css');
const githubCssPath = path.join(__dirname, '../assets/css/github-preview.css');
let styleCSS = '';
let githubCSS = '';
try {
styleCSS = fs.readFileSync(styleCssPath, 'utf8');
githubCSS = fs.readFileSync(githubCssPath, 'utf8');
} catch (err) {
console.error('Error reading CSS files:', err);
}
// Generate HTML content
const htmlContent = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${title}</title>
<link rel="stylesheet" href="style.css">
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
</head>
<body>
<div id="presentation-container">
<div class="presentation-header">
<h1>${title}</h1>
<div class="date">${new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}</div>
</div>
<div class="presentation-content markdown-body markdown-preview" id="content"></div>
</div>
<script>
// Render markdown content first
const content = ${JSON.stringify(content)};
const contentDiv = document.getElementById('content');
contentDiv.innerHTML = marked.parse(content);
// Initialize Mermaid
mermaid.initialize({
startOnLoad: false,
theme: 'default',
securityLevel: 'loose'
});
// Process Mermaid diagrams
setTimeout(() => {
const mermaidBlocks = contentDiv.querySelectorAll('code.language-mermaid, pre code.language-mermaid');
mermaidBlocks.forEach((block, index) => {
try {
const mermaidCode = block.textContent;
const uniqueId = 'mermaid-' + Date.now() + '-' + index;
const mermaidDiv = document.createElement('div');
mermaidDiv.id = uniqueId;
mermaidDiv.className = 'mermaid';
mermaidDiv.textContent = mermaidCode;
const pre = block.closest('pre') || block;
pre.parentNode.replaceChild(mermaidDiv, pre);
mermaid.render(uniqueId + '-svg', mermaidCode).then(function(result) {
mermaidDiv.innerHTML = result.svg;
}).catch(function(err) {
console.warn('Mermaid rendering error:', err);
mermaidDiv.innerHTML = '<pre style="color: red;">Mermaid error: ' + err.message + '</pre>';
});
} catch (error) {
console.warn('Mermaid processing error:', error);
}
});
}, 200);
</script>
</body>
</html>`;
// Combine CSS with additional presentation styles
const combinedCSS = `
${styleCSS}
${githubCSS}
/* Additional presentation styles */
body {
margin: 0;
padding: 0;
}
#presentation-container {
max-width: 900px;
margin: 0 auto;
padding: 3rem 2rem;
min-height: 100vh;
}
.presentation-header {
text-align: center;
padding: 2rem 0;
border-bottom: 2px solid var(--border-color);
margin-bottom: 3rem;
}
.presentation-header h1 {
margin: 0 0 0.5rem 0;
color: var(--primary-color);
}
.presentation-header .date {
color: var(--text-light);
font-size: 0.9rem;
}
.presentation-content {
line-height: 1.8;
}
`;
// Create ZIP archive
const archive = archiver('zip', {
zlib: { level: 9 } // Maximum compression
});
res.setHeader('Content-Type', 'application/zip');
res.setHeader('Content-Disposition', `attachment; filename="${sanitizeFilename(title)}-web.zip"`);
// Pipe archive to response
archive.pipe(res);
// Add files to archive
archive.append(htmlContent, { name: 'index.html' });
archive.append(combinedCSS, { name: 'style.css' });
// Finalize archive
await archive.finalize();
logger.info('EXPORT', `Web ZIP generated successfully in ${Date.now() - startTime}ms`, { title });
} catch (error) {
logger.error('EXPORT', 'Web export error', { error: error.message, title });
res.status(500).json({
success: false,
error: 'Error during web export'
});
}
});
module.exports = router;