diff --git a/app.js b/app.js
index 06e4d12..54c063a 100644
--- a/app.js
+++ b/app.js
@@ -3,6 +3,8 @@ const path = require('path');
const indexRoutes = require('./routes/index');
const apiRoutes = require('./routes/api');
+const templatesRoutes = require('./routes/templates');
+const exportRoutes = require('./routes/export');
const app = express();
const port = process.env.PORT || 3000;
@@ -12,8 +14,17 @@ app.use(express.urlencoded({ extended: true }));
app.use('/assets', express.static(path.join(__dirname, 'assets')));
+// Créer le dossier data s'il n'existe pas
+const fs = require('fs');
+const dataDir = path.join(__dirname, 'data');
+if (!fs.existsSync(dataDir)) {
+ fs.mkdirSync(dataDir);
+}
+
app.use('/', indexRoutes);
app.use('/api', apiRoutes);
+app.use('/api/templates', templatesRoutes);
+app.use('/api/export', exportRoutes);
app.listen(port, () => {
console.log(`Server running on port ${port}`);
diff --git a/assets/css/style.css b/assets/css/style.css
index e69de29..84d1a4f 100644
--- a/assets/css/style.css
+++ b/assets/css/style.css
@@ -0,0 +1,998 @@
+/* Variables CSS */
+:root {
+ --primary-color: #2c3e50;
+ --secondary-color: #3498db;
+ --accent-color: #e74c3c;
+ --success-color: #27ae60;
+ --warning-color: #f39c12;
+ --background-color: #ecf0f1;
+ --surface-color: #ffffff;
+ --text-color: #2c3e50;
+ --text-light: #7f8c8d;
+ --border-color: #bdc3c7;
+ --shadow: 0 2px 4px rgba(0,0,0,0.1);
+ --shadow-hover: 0 4px 8px rgba(0,0,0,0.15);
+ --border-radius: 8px;
+ --transition: all 0.3s ease;
+}
+
+/* Mode sombre */
+body.dark-theme {
+ --primary-color: #ecf0f1;
+ --secondary-color: #3498db;
+ --background-color: #1a1a1a;
+ --surface-color: #2c2c2c;
+ --text-color: #ecf0f1;
+ --text-light: #95a5a6;
+ --border-color: #34495e;
+}
+
+/* Reset et base */
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
+ line-height: 1.6;
+ color: var(--text-color);
+ background-color: var(--background-color);
+ transition: var(--transition);
+}
+
+/* Layout principal */
+.container {
+ max-width: 1400px;
+ margin: 0 auto;
+ padding: 0 20px;
+}
+
+/* Header professionnel */
+header {
+ background: var(--surface-color);
+ border-bottom: 1px solid var(--border-color);
+ box-shadow: var(--shadow);
+ position: sticky;
+ top: 0;
+ z-index: 100;
+}
+
+header > div {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 2rem;
+ max-width: 1400px;
+ margin: 0 auto;
+}
+
+header h1 {
+ color: var(--primary-color);
+ font-size: 2.2rem;
+ font-weight: 600;
+ margin: 0;
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ text-align: center;
+ letter-spacing: -0.5px;
+}
+
+header h1::before {
+ content: "📝";
+ font-size: 2.4rem;
+}
+
+/* Panneaux latéraux rétractables */
+.side-panel {
+ position: fixed;
+ top: 50%;
+ transform: translateY(-50%);
+ background: var(--surface-color);
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius);
+ box-shadow: var(--shadow-hover);
+ z-index: 90;
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ backdrop-filter: blur(10px);
+}
+
+/* Panneau gauche - Configuration */
+.left-panel {
+ left: -320px;
+ width: 320px;
+ border-left: none;
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+}
+
+.left-panel.open {
+ transform: translateY(-50%) translateX(320px);
+}
+
+/* Panneau droit - Navigation */
+.right-panel {
+ right: -320px;
+ width: 320px;
+ border-right: none;
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+}
+
+.right-panel.open {
+ transform: translateY(-50%) translateX(-320px);
+}
+
+/* Contenu des panneaux */
+.panel-content {
+ padding: 2rem;
+}
+
+.panel-header {
+ margin: 0 0 1.5rem 0;
+ color: var(--primary-color);
+ font-size: 1.2rem;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ border-bottom: 2px solid var(--border-color);
+ padding-bottom: 1rem;
+}
+
+/* Flèches d'ouverture */
+.panel-toggle {
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 40px;
+ height: 80px;
+ background: var(--surface-color);
+ border: 1px solid var(--border-color);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 1.2rem;
+ color: var(--text-color);
+ transition: all 0.3s ease;
+ box-shadow: var(--shadow);
+ z-index: 91;
+}
+
+.left-panel .panel-toggle {
+ right: -40px;
+ border-left: none;
+ border-radius: 0 var(--border-radius) var(--border-radius) 0;
+}
+
+.right-panel .panel-toggle {
+ left: -40px;
+ border-right: none;
+ border-radius: var(--border-radius) 0 0 var(--border-radius);
+}
+
+.panel-toggle:hover {
+ background: var(--secondary-color);
+ color: white;
+ transform: translateY(-50%) scale(1.05);
+ box-shadow: var(--shadow-hover);
+}
+
+.panel-toggle:active {
+ transform: translateY(-50%) scale(0.95);
+}
+
+/* Rotation de la flèche selon l'état */
+.left-panel .panel-toggle .arrow {
+ transition: transform 0.3s ease;
+}
+
+.left-panel.open .panel-toggle .arrow {
+ transform: rotate(180deg);
+}
+
+.right-panel .panel-toggle .arrow {
+ transition: transform 0.3s ease;
+}
+
+.right-panel.open .panel-toggle .arrow {
+ transform: rotate(180deg);
+}
+
+/* Styles pour le formulaire de templates */
+.template-form {
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+}
+
+.form-group {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.form-group label {
+ font-size: 0.9rem;
+ color: var(--text-light);
+ font-weight: 500;
+}
+
+.form-group select {
+ padding: 0.75rem;
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius);
+ background: var(--surface-color);
+ color: var(--text-color);
+ font-size: 0.9rem;
+ cursor: pointer;
+ transition: var(--transition);
+}
+
+.form-group select:hover:not(:disabled) {
+ border-color: var(--secondary-color);
+}
+
+.form-group select:focus {
+ outline: none;
+ border-color: var(--secondary-color);
+ box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
+}
+
+.form-group select:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ background: var(--background-color);
+}
+
+.template-preview {
+ padding: 1rem;
+ background: var(--background-color);
+ border-radius: var(--border-radius);
+ border: 1px solid var(--border-color);
+}
+
+.template-preview h4 {
+ margin: 0 0 0.75rem 0;
+ color: var(--primary-color);
+ font-size: 0.95rem;
+}
+
+.preview-content {
+ font-size: 0.85rem;
+ color: var(--text-light);
+ line-height: 1.4;
+}
+
+.form-actions {
+ display: flex;
+ gap: 0.75rem;
+ flex-wrap: wrap;
+}
+
+.form-actions button {
+ flex: 1;
+ min-width: 120px;
+}
+
+/* Styles spécifiques au panneau de navigation */
+.nav-section {
+ margin-bottom: 2rem;
+}
+
+.nav-section h4 {
+ margin: 0 0 1rem 0;
+ font-size: 0.95rem;
+ color: var(--text-light);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.nav-buttons {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.nav-btn {
+ width: 100%;
+ padding: 1rem 1.5rem;
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius);
+ background: transparent;
+ color: var(--text-color);
+ cursor: pointer;
+ transition: var(--transition);
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ font-size: 0.9rem;
+ text-align: left;
+}
+
+.nav-btn:hover {
+ background: var(--background-color);
+ border-color: var(--secondary-color);
+ color: var(--secondary-color);
+ transform: translateX(5px);
+}
+
+.nav-btn .icon {
+ font-size: 1.2rem;
+ width: 24px;
+ text-align: center;
+}
+
+/* File input dans le panneau */
+.panel-file-input {
+ width: 100%;
+ padding: 1rem 1.5rem;
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius);
+ background: transparent;
+ color: var(--text-color);
+ cursor: pointer;
+ transition: var(--transition);
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ font-size: 0.9rem;
+ text-align: left;
+ overflow: hidden;
+}
+
+.panel-file-input:hover {
+ background: var(--background-color);
+ border-color: var(--warning-color);
+ color: var(--warning-color);
+ transform: translateX(5px);
+}
+
+.panel-file-input .icon {
+ font-size: 1.2rem;
+ width: 24px;
+ text-align: center;
+}
+
+/* Animation d'overlay quand les panneaux sont ouverts */
+.panel-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.1);
+ z-index: 89;
+ opacity: 0;
+ visibility: hidden;
+ transition: all 0.3s ease;
+ backdrop-filter: blur(2px);
+}
+
+.panel-overlay.active {
+ opacity: 1;
+ visibility: visible;
+}
+
+#skeleton-select {
+ flex: 1;
+ padding: 0.75rem 1rem;
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius);
+ background: var(--surface-color);
+ color: var(--text-color);
+ font-size: 0.9rem;
+ cursor: pointer;
+ transition: var(--transition);
+}
+
+#skeleton-select:hover {
+ border-color: var(--secondary-color);
+}
+
+#skeleton-select:focus {
+ outline: none;
+ border-color: var(--secondary-color);
+ box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
+}
+
+/* Responsive design */
+@media (max-width: 768px) {
+ .side-panel {
+ width: 280px;
+ top: 60%;
+ }
+
+ .left-panel {
+ left: -280px;
+ }
+
+ .left-panel.open {
+ transform: translateY(-60%) translateX(280px);
+ }
+
+ .right-panel {
+ right: -280px;
+ }
+
+ .right-panel.open {
+ transform: translateY(-60%) translateX(-280px);
+ }
+
+ .panel-content {
+ padding: 1.5rem;
+ }
+
+ .panel-toggle {
+ width: 35px;
+ height: 70px;
+ font-size: 1rem;
+ }
+
+ header h1 {
+ font-size: 1.8rem;
+ }
+
+ header h1::before {
+ font-size: 2rem;
+ }
+
+ header > div {
+ padding: 1.5rem;
+ }
+
+ main {
+ grid-template-columns: 1fr;
+ padding: 1rem;
+ }
+
+ #table-of-contents,
+ #ai-assistant {
+ position: static;
+ max-height: none;
+ }
+}
+
+@media (max-width: 480px) {
+ .side-panel {
+ width: calc(100vw - 40px);
+ left: -100vw;
+ right: -100vw;
+ }
+
+ .left-panel.open {
+ transform: translateY(-60%) translateX(calc(100vw - 20px));
+ }
+
+ .right-panel.open {
+ transform: translateY(-60%) translateX(calc(-100vw + 20px));
+ }
+}
+
+/* Boutons */
+button, .btn {
+ padding: 0.6rem 1.2rem;
+ border: none;
+ border-radius: 25px;
+ background: var(--secondary-color);
+ color: white;
+ font-size: 0.85rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: var(--transition);
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ text-decoration: none;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+}
+
+button:hover, .btn:hover {
+ background: #2980b9;
+ box-shadow: 0 4px 12px rgba(0,0,0,0.2);
+ transform: translateY(-2px);
+}
+
+button:active, .btn:active {
+ transform: translateY(0);
+}
+
+/* Boutons dans le header */
+header button, header .btn {
+ background: rgba(255,255,255,0.9);
+ color: var(--primary-color);
+ border: 1px solid rgba(255,255,255,0.3);
+ backdrop-filter: blur(10px);
+}
+
+header button:hover, header .btn:hover {
+ background: white;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15);
+ color: var(--secondary-color);
+}
+
+button.secondary {
+ background: rgba(255,255,255,0.9);
+ color: var(--primary-color);
+ border: 1px solid rgba(255,255,255,0.3);
+}
+
+button.secondary:hover {
+ background: white;
+ color: var(--secondary-color);
+}
+
+button.danger {
+ background: var(--accent-color);
+}
+
+button.danger:hover {
+ background: #c0392b;
+}
+
+button.success {
+ background: var(--success-color);
+}
+
+button.success:hover {
+ background: #229954;
+}
+
+/* Input file personnalis� */
+.file-input-label {
+ padding: 0.6rem 1.2rem;
+ border-radius: var(--border-radius);
+ background: var(--warning-color);
+ color: white;
+ font-size: 0.9rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: var(--transition);
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.file-input-label:hover {
+ background: #e67e22;
+ box-shadow: var(--shadow-hover);
+ transform: translateY(-1px);
+}
+
+/* Main content */
+main {
+ display: grid;
+ grid-template-columns: 300px 1fr 350px;
+ gap: 2rem;
+ padding: 2rem;
+ max-width: 1400px;
+ margin: 0 auto;
+ min-height: calc(100vh - 140px);
+}
+
+@media (max-width: 1200px) {
+ main {
+ grid-template-columns: 250px 1fr;
+ gap: 1.5rem;
+ }
+
+ #ai-assistant {
+ grid-column: 1 / -1;
+ margin-top: 2rem;
+ }
+}
+
+@media (max-width: 768px) {
+ main {
+ grid-template-columns: 1fr;
+ gap: 1rem;
+ padding: 1rem;
+ }
+}
+
+/* Sections */
+section {
+ background: var(--surface-color);
+ border-radius: var(--border-radius);
+ box-shadow: var(--shadow);
+ overflow: hidden;
+}
+
+section h2 {
+ background: var(--secondary-color);
+ color: white;
+ padding: 1rem 1.5rem;
+ margin: 0;
+ font-size: 1.2rem;
+ font-weight: 600;
+}
+
+/* Table des mati�res */
+#table-of-contents {
+ position: sticky;
+ top: 100px;
+ max-height: calc(100vh - 120px);
+ overflow-y: auto;
+}
+
+#toc-nav {
+ padding: 1.5rem;
+}
+
+#toc-nav ul {
+ list-style: none;
+}
+
+#toc-nav li {
+ margin-bottom: 0.5rem;
+}
+
+#toc-nav a {
+ color: var(--text-color);
+ text-decoration: none;
+ padding: 0.3rem 0.5rem;
+ border-radius: 4px;
+ display: block;
+ transition: var(--transition);
+ font-size: 0.9rem;
+}
+
+#toc-nav a:hover {
+ background: var(--background-color);
+ color: var(--secondary-color);
+}
+
+#toc-nav ul ul {
+ margin-left: 1rem;
+ margin-top: 0.5rem;
+}
+
+#toc-nav ul ul a {
+ font-size: 0.85rem;
+ color: var(--text-light);
+}
+
+/* Zone d'�criture */
+#design-journal {
+ min-height: 600px;
+}
+
+#journal-editor {
+ min-height: 500px;
+ padding: 2rem;
+ font-size: 1rem;
+ line-height: 1.8;
+ outline: none;
+ border: none;
+ background: var(--surface-color);
+ color: var(--text-color);
+ resize: vertical;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ overflow-wrap: break-word;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
+}
+
+#journal-editor:focus {
+ background: var(--background-color);
+}
+
+#journal-editor:empty::before {
+ content: "Commencez à écrire votre journal de conception...\A\A# Titre de votre projet\A\A## Contexte et objectifs\A\ADécrivez ici le contexte de votre projet...\A\A## Architecture\A\A### Composants principaux\A\A### Technologies utilisées\A\A## Décisions de conception\A\A### Décision 1\A\A**Problème :**\A**Options considérées :**\A**Décision :**\A**Justification :**\A\A## Prochaines étapes\A\A- [ ] Tâche 1\A- [ ] Tâche 2";
+ color: var(--text-light);
+ font-style: italic;
+ white-space: pre-wrap;
+ pointer-events: none;
+}
+
+#journal-editor p {
+ margin-bottom: 1rem;
+}
+
+#journal-editor h1, #journal-editor h2, #journal-editor h3 {
+ color: var(--primary-color);
+ margin: 1.5rem 0 1rem 0;
+}
+
+#journal-editor h1 {
+ font-size: 2rem;
+ border-bottom: 3px solid var(--secondary-color);
+ padding-bottom: 0.5rem;
+}
+
+#journal-editor h2 {
+ font-size: 1.6rem;
+ border-bottom: 2px solid var(--border-color);
+ padding-bottom: 0.3rem;
+}
+
+#journal-editor h3 {
+ font-size: 1.3rem;
+ color: var(--secondary-color);
+}
+
+/* Assistant IA */
+#ai-assistant {
+ position: sticky;
+ top: 100px;
+ max-height: calc(100vh - 120px);
+ display: flex;
+ flex-direction: column;
+}
+
+/* Styles pour "Coming Soon" */
+.ai-coming-soon {
+ padding: 1.5rem;
+ background: linear-gradient(135deg, var(--background-color), var(--surface-color));
+}
+
+.coming-soon-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ background: linear-gradient(135deg, var(--warning-color), #f39c12);
+ color: white;
+ padding: 0.5rem 1rem;
+ border-radius: 50px;
+ font-size: 0.85rem;
+ font-weight: 600;
+ margin-bottom: 1.5rem;
+ box-shadow: 0 2px 8px rgba(243, 156, 18, 0.3);
+ animation: glow 2s ease-in-out infinite alternate;
+}
+
+@keyframes glow {
+ from { box-shadow: 0 2px 8px rgba(243, 156, 18, 0.3); }
+ to { box-shadow: 0 4px 16px rgba(243, 156, 18, 0.5); }
+}
+
+.badge-icon {
+ font-size: 1rem;
+ animation: bounce 1s ease-in-out infinite;
+}
+
+@keyframes bounce {
+ 0%, 100% { transform: translateY(0); }
+ 50% { transform: translateY(-2px); }
+}
+
+.ai-preview h3 {
+ color: var(--primary-color);
+ font-size: 1rem;
+ margin: 0 0 1rem 0;
+ font-weight: 600;
+}
+
+.ai-features-list {
+ list-style: none;
+ padding: 0;
+ margin: 0 0 1.5rem 0;
+}
+
+.ai-features-list li {
+ padding: 0.75rem 0;
+ border-bottom: 1px solid var(--border-color);
+ font-size: 0.9rem;
+ line-height: 1.4;
+ display: flex;
+ align-items: flex-start;
+ gap: 0.5rem;
+}
+
+.ai-features-list li:last-child {
+ border-bottom: none;
+}
+
+.ai-tech-stack {
+ background: var(--surface-color);
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius);
+ padding: 1rem;
+ margin-top: 1rem;
+}
+
+.ai-tech-stack p {
+ margin: 0.25rem 0;
+ font-size: 0.85rem;
+ color: var(--text-light);
+}
+
+.ai-tech-stack strong {
+ color: var(--primary-color);
+}
+
+#ai-assistant-feedback {
+ flex: 1;
+ padding: 1.5rem;
+ border-top: 1px solid var(--border-color);
+ background: var(--background-color);
+ overflow-y: auto;
+ min-height: 200px;
+}
+
+.feedback-message {
+ background: var(--surface-color);
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius);
+ padding: 1rem;
+ margin-bottom: 1rem;
+ font-size: 0.9rem;
+ line-height: 1.5;
+}
+
+.coming-soon-message {
+ background: linear-gradient(135deg, var(--surface-color), var(--background-color));
+ border: 2px dashed var(--warning-color);
+ text-align: center;
+}
+
+.feedback-message.loading {
+ background: var(--background-color);
+ border-color: var(--secondary-color);
+ animation: pulse 1.5s infinite;
+}
+
+@keyframes pulse {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.7; }
+}
+
+/* Footer */
+footer {
+ background: var(--secondary-color);
+ color: white;
+ text-align: center;
+ padding: 2rem;
+ margin-top: 3rem;
+}
+
+footer a {
+ color: white;
+ text-decoration: underline;
+}
+
+footer a:hover {
+ color: var(--secondary-color);
+}
+
+/* �tats de chargement */
+.loading {
+ position: relative;
+ opacity: 0.7;
+ pointer-events: none;
+}
+
+.loading::after {
+ content: '';
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 20px;
+ height: 20px;
+ margin: -10px 0 0 -10px;
+ border: 2px solid var(--border-color);
+ border-top: 2px solid var(--secondary-color);
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+/* Notifications */
+.notification {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ padding: 1rem 1.5rem;
+ background: var(--success-color);
+ color: white;
+ border-radius: var(--border-radius);
+ box-shadow: var(--shadow-hover);
+ z-index: 1000;
+ transform: translateX(400px);
+ transition: var(--transition);
+}
+
+.notification.show {
+ transform: translateX(0);
+}
+
+.notification.error {
+ background: var(--accent-color);
+}
+
+.notification.warning {
+ background: var(--warning-color);
+}
+
+/* Responsive design */
+@media (max-width: 768px) {
+ header > div {
+ flex-direction: column;
+ gap: 1rem;
+ }
+
+ .header-controls {
+ flex-wrap: wrap;
+ justify-content: center;
+ }
+
+ nav {
+ flex-wrap: wrap;
+ justify-content: center;
+ }
+
+ button, .btn {
+ padding: 0.5rem 1rem;
+ font-size: 0.85rem;
+ }
+}
+
+/* Scrollbars personnalis�es */
+::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+}
+
+::-webkit-scrollbar-track {
+ background: var(--background-color);
+}
+
+::-webkit-scrollbar-thumb {
+ background: var(--border-color);
+ border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: var(--text-light);
+}
+
+/* S�lection de texte */
+::selection {
+ background: var(--secondary-color);
+ color: white;
+}
+
+/* Focus outline */
+button:focus-visible,
+input:focus-visible,
+select:focus-visible {
+ outline: 2px solid var(--secondary-color);
+ outline-offset: 2px;
+}
+
+/* Utilitaires */
+.hidden {
+ display: none !important;
+}
+
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ border: 0;
+}
+
+.text-center {
+ text-align: center;
+}
+
+.mb-1 { margin-bottom: 0.5rem; }
+.mb-2 { margin-bottom: 1rem; }
+.mb-3 { margin-bottom: 1.5rem; }
+
+.mt-1 { margin-top: 0.5rem; }
+.mt-2 { margin-top: 1rem; }
+.mt-3 { margin-top: 1.5rem; }
\ No newline at end of file
diff --git a/assets/js/app.js b/assets/js/app.js
new file mode 100644
index 0000000..21f47c9
--- /dev/null
+++ b/assets/js/app.js
@@ -0,0 +1,583 @@
+// Application principale
+class ConceptionAssistant {
+ constructor() {
+ this.currentJournalId = null;
+ this.editor = null;
+ this.autoSaveTimer = null;
+ this.init();
+ }
+
+ async init() {
+ this.setupEditor();
+ this.setupEventListeners();
+ this.setupAutoSave();
+ await this.loadJournalList();
+ this.generateTOC();
+ }
+
+ setupEditor() {
+ this.editor = document.getElementById('journal-editor');
+
+ // Générer TOC à chaque modification
+ this.editor.addEventListener('input', () => {
+ this.generateTOC();
+ this.resetAutoSave();
+ });
+
+ // Gestion des raccourcis clavier
+ this.editor.addEventListener('keydown', (e) => {
+ // Ctrl+S pour sauvegarder
+ if (e.ctrlKey && e.key === 's') {
+ e.preventDefault();
+ this.saveJournal();
+ }
+
+ // Tab pour indentation
+ if (e.key === 'Tab') {
+ e.preventDefault();
+ document.execCommand('insertText', false, ' ');
+ }
+ });
+ }
+
+ showEditorPlaceholder() {
+ // Le placeholder est maintenant géré via CSS ::before
+ // Cette fonction peut être supprimée ou laissée vide pour compatibilité
+ }
+
+ setupEventListeners() {
+ // Boutons de contrôle des journaux
+ document.getElementById('save-journal')?.addEventListener('click', () => this.saveJournal());
+ document.getElementById('load-journal')?.addEventListener('click', () => this.showJournalSelector());
+
+ // Table des matières
+ document.getElementById('refresh-toc')?.addEventListener('click', () => this.generateTOC());
+
+ // Export/Import
+ document.getElementById('export-md')?.addEventListener('click', () => this.exportMarkdown());
+ document.getElementById('export-pdf')?.addEventListener('click', () => this.exportPDF());
+ document.getElementById('import-md')?.addEventListener('change', (e) => this.importMarkdown(e));
+
+ // Thème
+ document.getElementById('theme-toggle')?.addEventListener('click', () => this.toggleTheme());
+
+ // Assistant IA (simulé pour le MVP)
+ document.getElementById('activate-rephrase')?.addEventListener('click', () => this.handleAI('rephrase'));
+ document.getElementById('check-inconsistencies')?.addEventListener('click', () => this.handleAI('inconsistencies'));
+ document.getElementById('check-duplications')?.addEventListener('click', () => this.handleAI('duplications'));
+ document.getElementById('give-advice')?.addEventListener('click', () => this.handleAI('advice'));
+ document.getElementById('liberty-mode')?.addEventListener('click', () => this.handleAI('liberty'));
+ }
+
+ setupAutoSave() {
+ // Sauvegarde automatique toutes les 30 secondes
+ setInterval(() => {
+ if (this.currentJournalId && this.editor.innerText.trim()) {
+ this.saveJournal(true);
+ }
+ }, 30000);
+ }
+
+ resetAutoSave() {
+ if (this.autoSaveTimer) {
+ clearTimeout(this.autoSaveTimer);
+ }
+
+ this.autoSaveTimer = setTimeout(() => {
+ if (this.currentJournalId && this.editor.innerText.trim()) {
+ this.saveJournal(true);
+ }
+ }, 3000);
+ }
+
+ async saveJournal(isAutoSave = false) {
+ const content = this.editor.innerText;
+ if (!content) return;
+
+ const statusEl = document.getElementById('save-status');
+ const saveBtn = document.getElementById('save-journal');
+
+ if (!isAutoSave) {
+ saveBtn.classList.add('loading');
+ statusEl.textContent = 'Sauvegarde...';
+ }
+
+ try {
+ let response;
+
+ if (this.currentJournalId) {
+ // Mise à jour
+ response = await fetch(`/api/journals/${this.currentJournalId}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ content })
+ });
+ } else {
+ // Création
+ response = await fetch('/api/journals', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ content })
+ });
+ }
+
+ const result = await response.json();
+
+ if (result.success) {
+ if (!this.currentJournalId) {
+ this.currentJournalId = result.data.id;
+ }
+
+ if (!isAutoSave) {
+ statusEl.textContent = '✅ Sauvegardé';
+ this.showNotification('Journal sauvegardé avec succès', 'success');
+ setTimeout(() => statusEl.textContent = '', 3000);
+ } else {
+ statusEl.textContent = '💾 Auto-sauvegardé';
+ setTimeout(() => statusEl.textContent = '', 2000);
+ }
+ } else {
+ throw new Error(result.error || 'Erreur de sauvegarde');
+ }
+ } catch (error) {
+ console.error('Erreur:', error);
+ statusEl.textContent = '❌ Erreur';
+ if (!isAutoSave) {
+ this.showNotification('Erreur lors de la sauvegarde: ' + error.message, 'error');
+ }
+ setTimeout(() => statusEl.textContent = '', 3000);
+ } finally {
+ if (!isAutoSave) {
+ saveBtn.classList.remove('loading');
+ }
+ }
+ }
+
+ async loadJournalList() {
+ try {
+ const response = await fetch('/api/journals');
+ const result = await response.json();
+
+ if (result.success && result.data.length > 0) {
+ // Charger le dernier journal automatiquement
+ const lastJournal = result.data[result.data.length - 1];
+ await this.loadJournal(lastJournal.id);
+ }
+ } catch (error) {
+ console.error('Erreur chargement liste:', error);
+ }
+ }
+
+ async loadJournal(id) {
+ try {
+ const response = await fetch(`/api/journals/${id}`);
+ const result = await response.json();
+
+ if (result.success && result.data.length > 0) {
+ const journal = result.data[0];
+ this.currentJournalId = id;
+ this.editor.innerText = journal.markdownContent;
+ this.generateTOC();
+ this.showNotification('Journal chargé', 'success');
+ }
+ } catch (error) {
+ console.error('Erreur chargement journal:', error);
+ this.showNotification('Erreur lors du chargement', 'error');
+ }
+ }
+
+ async showJournalSelector() {
+ try {
+ const response = await fetch('/api/journals');
+ const result = await response.json();
+
+ if (result.success) {
+ const journalList = result.data.map(journal => {
+ const preview = journal.markdownContent.substring(0, 100).replace(/\n/g, ' ');
+ return `${preview}... `;
+ }).join('');
+
+ const feedback = document.getElementById('ai-assistant-feedback');
+ feedback.innerHTML = `
+
+
📂 Sélectionner un journal
+
+ + Nouveau journal
+ ${journalList}
+
+
+ `;
+
+ // Ajouter les event listeners
+ document.querySelectorAll('.journal-item').forEach(btn => {
+ btn.addEventListener('click', () => {
+ this.loadJournal(btn.dataset.id);
+ this.clearFeedback();
+ });
+ });
+ }
+ } catch (error) {
+ console.error('Erreur:', error);
+ this.showNotification('Erreur lors du chargement de la liste', 'error');
+ }
+ }
+
+ createNewJournal() {
+ this.currentJournalId = null;
+ this.editor.innerText = '';
+ this.generateTOC();
+ this.clearFeedback();
+ this.showNotification('Nouveau journal créé', 'success');
+ }
+
+ generateTOC() {
+ const content = this.editor.innerText;
+ const lines = content.split('\n');
+ const toc = [];
+ let tocHtml = '';
+
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i].trim();
+ if (line.startsWith('#')) {
+ const level = line.match(/^#+/)[0].length;
+ const title = line.replace(/^#+\s*/, '');
+ const id = 'heading-' + i;
+
+ toc.push({ level, title, id });
+ }
+ }
+
+ if (toc.length > 0) {
+ tocHtml = '';
+ let currentLevel = 0;
+
+ for (const item of toc) {
+ if (item.level > currentLevel) {
+ for (let i = currentLevel; i < item.level - 1; i++) {
+ tocHtml += '';
+ }
+ } else if (item.level < currentLevel) {
+ for (let i = item.level; i < currentLevel; i++) {
+ tocHtml += ' ';
+ }
+ }
+
+ tocHtml += `${item.title} `;
+ currentLevel = item.level;
+ }
+
+ tocHtml += ' ';
+ } else {
+ tocHtml = 'Ajoutez des titres (# ## ###) à votre journal pour générer la table des matières.
';
+ }
+
+ document.getElementById('toc-nav').innerHTML = tocHtml;
+ }
+
+ scrollToHeading(headingId) {
+ // Simulation du scroll vers les titres
+ this.showNotification('Navigation vers la section', 'success');
+ }
+
+ exportMarkdown() {
+ const content = this.editor.innerText;
+ if (!content.trim()) {
+ this.showNotification('Aucun contenu à exporter', 'warning');
+ return;
+ }
+
+ const blob = new Blob([content], { type: 'text/markdown' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = 'journal-conception.md';
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+
+ this.showNotification('Fichier Markdown exporté', 'success');
+ }
+
+ exportPDF() {
+ // Simulation export PDF (nécessite une vraie implémentation côté serveur)
+ this.showNotification('Export PDF : fonctionnalité à venir', 'warning');
+ }
+
+ importMarkdown(event) {
+ const file = event.target.files[0];
+ if (!file) return;
+
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ this.editor.innerText = e.target.result;
+ this.generateTOC();
+ this.currentJournalId = null; // Nouveau journal
+ this.showNotification('Fichier Markdown importé', 'success');
+ };
+ reader.readAsText(file);
+ }
+
+ toggleTheme() {
+ document.body.classList.toggle('dark-theme');
+ const isDark = document.body.classList.contains('dark-theme');
+ localStorage.setItem('theme', isDark ? 'dark' : 'light');
+ this.showNotification(`Mode ${isDark ? 'sombre' : 'clair'} activé`, 'success');
+ }
+
+ handleAI(action) {
+ const selection = window.getSelection().toString();
+ let message = '';
+
+ switch (action) {
+ case 'rephrase':
+ if (!selection) {
+ message = '⚠️ Sélectionnez du texte à reformuler';
+ } else {
+ message = `✨ Reformulation suggérée pour:"${selection.substring(0, 100)}..." Version reformulée : "${this.simulateRephrase(selection)}"`;
+ }
+ break;
+ case 'inconsistencies':
+ message = '🔍 Analyse des incohérences... ✅ Aucune incohérence majeure détectée dans le document.';
+ break;
+ case 'duplications':
+ message = '📋 Vérification des duplications... ✅ Aucune duplication significative trouvée.';
+ break;
+ case 'advice':
+ message = '💡 Conseils pour améliorer votre journal : • Ajoutez plus de détails sur les alternatives considérées • Documentez les raisons des choix techniques • Incluez des diagrammes ou schémas • Ajoutez une section "Lessons learned"';
+ break;
+ case 'liberty':
+ const count = document.getElementById('liberty-repeat-count').value || 3;
+ message = `🚀 Mode Liberté activé (${count} itérations) Génération de contenus automatiques basée sur le contexte existant...Cette fonctionnalité nécessite une intégration IA complète. `;
+ break;
+ }
+
+ this.showAIFeedback(message);
+ }
+
+ simulateRephrase(text) {
+ // Simulation simple de reformulation
+ const replacements = {
+ 'nous devons': 'il convient de',
+ 'c\'est important': 'cela revêt une importance',
+ 'il faut': 'il est nécessaire de',
+ 'très': 'particulièrement',
+ 'beaucoup': 'considérablement'
+ };
+
+ let result = text;
+ Object.entries(replacements).forEach(([from, to]) => {
+ result = result.replace(new RegExp(from, 'gi'), to);
+ });
+
+ return result || text;
+ }
+
+ showAIFeedback(message) {
+ const feedback = document.getElementById('ai-assistant-feedback');
+ feedback.innerHTML = `${message}
`;
+ }
+
+ clearFeedback() {
+ const feedback = document.getElementById('ai-assistant-feedback');
+ feedback.innerHTML = `
+
+ 🎯 Assistant prêt
+ Sélectionnez du texte dans l'éditeur et cliquez sur une action pour commencer.
+
+ `;
+ }
+
+ showNotification(message, type = 'success') {
+ const notification = document.createElement('div');
+ notification.className = `notification ${type}`;
+ notification.textContent = message;
+
+ document.body.appendChild(notification);
+
+ setTimeout(() => notification.classList.add('show'), 100);
+ setTimeout(() => {
+ notification.classList.remove('show');
+ setTimeout(() => document.body.removeChild(notification), 300);
+ }, 3000);
+ }
+}
+
+// Gestion des panneaux latéraux
+function togglePanel(side) {
+ const panel = document.getElementById(`${side}-panel`);
+ const overlay = document.getElementById('panel-overlay');
+ const otherPanel = document.getElementById(side === 'left' ? 'right-panel' : 'left-panel');
+
+ // Fermer l'autre panneau s'il est ouvert
+ if (otherPanel && otherPanel.classList.contains('open')) {
+ otherPanel.classList.remove('open');
+ }
+
+ // Toggle le panneau actuel
+ if (panel.classList.contains('open')) {
+ panel.classList.remove('open');
+ overlay.classList.remove('active');
+ } else {
+ panel.classList.add('open');
+ overlay.classList.add('active');
+ }
+}
+
+function closeAllPanels() {
+ const leftPanel = document.getElementById('left-panel');
+ const rightPanel = document.getElementById('right-panel');
+ const overlay = document.getElementById('panel-overlay');
+
+ if (leftPanel) leftPanel.classList.remove('open');
+ if (rightPanel) rightPanel.classList.remove('open');
+ if (overlay) overlay.classList.remove('active');
+}
+
+// Fermer les panneaux avec Escape
+document.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape') {
+ closeAllPanels();
+ }
+});
+
+// Initialisation de l'application
+let app;
+document.addEventListener('DOMContentLoaded', () => {
+ app = new ConceptionAssistant();
+
+ // Charger le thème sauvegardé
+ const savedTheme = localStorage.getItem('theme');
+ if (savedTheme === 'dark') {
+ document.body.classList.add('dark-theme');
+ }
+
+ // Initialiser les panneaux
+ initializePanels();
+});
+
+function initializePanels() {
+ // Initialiser la gestion des templates
+ initializeTemplateForm();
+}
+
+function initializeTemplateForm() {
+ const domainSelect = document.getElementById('domain-select');
+ const levelSelect = document.getElementById('level-select');
+ const loadTemplateBtn = document.getElementById('load-template');
+ const resetTemplateBtn = document.getElementById('reset-template');
+ const templatePreview = document.getElementById('template-preview');
+ const previewContent = document.getElementById('preview-content');
+
+ // Gestion du changement de domaine
+ if (domainSelect) {
+ domainSelect.addEventListener('change', async () => {
+ const domain = domainSelect.value;
+
+ if (!domain) {
+ levelSelect.disabled = true;
+ levelSelect.innerHTML = '-- Choisir d\'abord un domaine -- ';
+ loadTemplateBtn.disabled = true;
+ templatePreview.style.display = 'none';
+ return;
+ }
+
+ // Activer le select de niveau
+ levelSelect.disabled = false;
+ levelSelect.innerHTML = `
+ -- Choisir le niveau --
+ 📄 Simple
+ 📋 Détaillé
+ 📚 Complet
+ `;
+
+ // Cacher l'aperçu
+ templatePreview.style.display = 'none';
+ loadTemplateBtn.disabled = true;
+ });
+ }
+
+ // Gestion du changement de niveau
+ if (levelSelect) {
+ levelSelect.addEventListener('change', async () => {
+ const domain = domainSelect.value;
+ const level = levelSelect.value;
+
+ if (!domain || !level) {
+ templatePreview.style.display = 'none';
+ loadTemplateBtn.disabled = true;
+ return;
+ }
+
+ try {
+ // Charger l'aperçu du template
+ const response = await fetch(`/api/templates/${domain}/${level}`);
+ const result = await response.json();
+
+ if (result.success) {
+ // Afficher un aperçu (premières lignes)
+ const lines = result.data.content.split('\n');
+ const preview = lines.slice(0, 8).join('\n');
+
+ previewContent.textContent = preview + (lines.length > 8 ? '\n...' : '');
+ templatePreview.style.display = 'block';
+ loadTemplateBtn.disabled = false;
+ } else {
+ app.showNotification('Erreur lors du chargement de l\'aperçu', 'error');
+ }
+ } catch (error) {
+ console.error('Erreur:', error);
+ app.showNotification('Erreur lors du chargement de l\'aperçu', 'error');
+ }
+ });
+ }
+
+ // Gestion du chargement du template
+ if (loadTemplateBtn) {
+ loadTemplateBtn.addEventListener('click', async () => {
+ const domain = domainSelect.value;
+ const level = levelSelect.value;
+
+ if (!domain || !level) {
+ app.showNotification('Veuillez sélectionner un domaine et un niveau', 'warning');
+ return;
+ }
+
+ try {
+ loadTemplateBtn.classList.add('loading');
+
+ const response = await fetch(`/api/templates/${domain}/${level}`);
+ const result = await response.json();
+
+ if (result.success) {
+ // Charger le template dans l'éditeur
+ app.editor.innerText = result.data.content;
+ app.generateTOC();
+ app.currentJournalId = null; // Nouveau journal
+
+ app.showNotification(`Template ${domain}/${level} chargé avec succès`, 'success');
+ closeAllPanels();
+ } else {
+ app.showNotification('Erreur lors du chargement du template', 'error');
+ }
+ } catch (error) {
+ console.error('Erreur:', error);
+ app.showNotification('Erreur lors du chargement du template', 'error');
+ } finally {
+ loadTemplateBtn.classList.remove('loading');
+ }
+ });
+ }
+
+ // Gestion du reset
+ if (resetTemplateBtn) {
+ resetTemplateBtn.addEventListener('click', () => {
+ domainSelect.value = '';
+ levelSelect.value = '';
+ levelSelect.disabled = true;
+ levelSelect.innerHTML = '-- Choisir d\'abord un domaine -- ';
+ templatePreview.style.display = 'none';
+ loadTemplateBtn.disabled = true;
+ });
+ }
+}
\ No newline at end of file
diff --git a/config/.env.example b/config/.env.example
index ab6c4f1..d87c8e8 100644
--- a/config/.env.example
+++ b/config/.env.example
@@ -1,2 +1,21 @@
# Server Configuration
-PORT=3000
\ No newline at end of file
+PORT=3000
+
+# Mistral AI Configuration
+MISTRAL_API_KEY=your_mistral_api_key_here
+MISTRAL_MODEL=mistral-large-latest
+MISTRAL_BASE_URL=https://api.mistral.ai/v1
+
+# AI Features Configuration
+AI_ENABLED=false
+AI_MAX_TOKENS=4000
+AI_TEMPERATURE=0.7
+AI_TOP_P=0.95
+
+# Rate Limiting for AI
+AI_RATE_LIMIT_REQUESTS=10
+AI_RATE_LIMIT_WINDOW=60000
+
+# PDF Export Configuration
+PDF_MAX_SIZE=10485760
+PDF_TIMEOUT=30000
\ No newline at end of file
diff --git a/package.json b/package.json
index 4808722..90f08b5 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,8 @@
"test": "node tests/all.test.js"
},
"dependencies": {
- "uuid": "^13.0.0",
- "express": "^4.18.2"
+ "express": "^4.18.2",
+ "puppeteer": "^24.22.3",
+ "uuid": "^13.0.0"
}
-}
\ No newline at end of file
+}
diff --git a/routes/api.js b/routes/api.js
index d80d072..efc2b66 100644
--- a/routes/api.js
+++ b/routes/api.js
@@ -4,6 +4,9 @@ const fs = require('fs');
const path = require('path');
const { v4: uuidv4 } = require('uuid');
+// Importer le module d'export
+const exportRouter = require('./export');
+
function modifMd(id, modifications) {
if (id === undefined) throw new Error('id obligatoire');
if (!Array.isArray(modifications) || modifications.length === 0) throw new Error('modifications requises');
@@ -101,7 +104,7 @@ function readMd(id = undefined) {
}
}
-function modifMd(id, newMarkdownContent) {
+function updateMd(id, newMarkdownContent) {
if (id === undefined) throw new Error('id obligatoire');
const dataDir = path.resolve(__dirname, '../data');
const mapPath = path.join(dataDir, 'uuid_map.json');
@@ -112,7 +115,7 @@ function modifMd(id, newMarkdownContent) {
if (!uuid) throw new Error(`Aucun fichier trouvé pour l'id ${id}`);
const mdPath = path.join(dataDir, `${uuid}.md`);
- if (!fs.existsSync(mdPath)) throw new Error('Le fichier markdown n’existe pas');
+ if (!fs.existsSync(mdPath)) throw new Error('Le fichier markdown n\'existe pas');
fs.writeFileSync(mdPath, newMarkdownContent, { encoding: 'utf8', flag: 'w' });
return { id, uuid, path: mdPath, newMarkdownContent };
}
@@ -166,12 +169,33 @@ router.get('/journals/:id', (req, res) => {
// PUT /api/journals/:id - Mettre à jour un journal
router.put('/journals/:id', (req, res) => {
const { id } = req.params;
- const { start, end, content } = req.body;
+ const { content, modifications } = req.body;
- res.json({
- success: true,
- data: modifMd(id, {start, end, content})
- });
+ try {
+ let result;
+ if (content) {
+ // Mise à jour complète du contenu
+ result = updateMd(id, content);
+ } else if (modifications) {
+ // Modifications partielles (pour compatibilité future)
+ result = modifMd(id, modifications);
+ } else {
+ return res.status(400).json({
+ success: false,
+ error: 'Content ou modifications requis'
+ });
+ }
+
+ res.json({
+ success: true,
+ data: result
+ });
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: error.message
+ });
+ }
});
// DELETE /api/journals/:id - Supprimer un journal
@@ -184,4 +208,7 @@ router.delete('/journals/:id', (req, res) => {
});
});
+// Intégrer les routes d'export
+router.use('/export', exportRouter);
+
module.exports = router;
\ No newline at end of file
diff --git a/routes/export.js b/routes/export.js
new file mode 100644
index 0000000..065060e
--- /dev/null
+++ b/routes/export.js
@@ -0,0 +1,512 @@
+const express = require('express');
+const router = express.Router();
+const puppeteer = require('puppeteer');
+const path = require('path');
+
+// POST /api/export/pdf - Générer un PDF depuis le contenu markdown
+router.post('/pdf', async (req, res) => {
+ const { content, title = 'Journal de Conception' } = req.body;
+
+ if (!content || content.trim() === '') {
+ return res.status(400).json({
+ success: false,
+ error: 'Contenu requis pour générer le PDF'
+ });
+ }
+
+ let browser;
+ try {
+ // Convertir le markdown en HTML avec styles
+ const htmlContent = generateStyledHTML(content, title);
+
+ // Lancer Puppeteer
+ browser = await puppeteer.launch({
+ headless: true,
+ args: ['--no-sandbox', '--disable-setuid-sandbox']
+ });
+
+ const page = await browser.newPage();
+
+ // Définir le contenu HTML
+ await page.setContent(htmlContent, {
+ waitUntil: 'domcontentloaded',
+ timeout: parseInt(process.env.PDF_TIMEOUT) || 30000
+ });
+
+ // Attendre que Mermaid soit chargé et les diagrammes rendus
+ await new Promise(resolve => setTimeout(resolve, 2000));
+
+ // Vérifier si des diagrammes Mermaid existent et attendre qu'ils se chargent
+ 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...');
+ }
+
+ // Générer le PDF
+ const pdfBuffer = await page.pdf({
+ format: 'A4',
+ margin: {
+ top: '2cm',
+ right: '2cm',
+ bottom: '2cm',
+ left: '2cm'
+ },
+ printBackground: true,
+ displayHeaderFooter: true,
+ headerTemplate: `
+
+ ${title}
+
+ `,
+ footerTemplate: `
+
+ Page sur - Généré le ${new Date().toLocaleDateString('fr-FR')}
+
+ `
+ });
+
+ await browser.close();
+
+ // Vérifier la taille du PDF
+ const maxSize = parseInt(process.env.PDF_MAX_SIZE) || 10485760; // 10MB par défaut
+ if (pdfBuffer.length > maxSize) {
+ return res.status(413).json({
+ success: false,
+ error: 'Le PDF généré dépasse la taille maximale autorisée'
+ });
+ }
+
+ // Envoyer le PDF
+ res.setHeader('Content-Type', 'application/pdf');
+ res.setHeader('Content-Disposition', `attachment; filename="${sanitizeFilename(title)}.pdf"`);
+ res.setHeader('Content-Length', pdfBuffer.length);
+
+ res.send(pdfBuffer);
+
+ } catch (error) {
+ console.error('Erreur génération PDF:', error);
+
+ if (browser) {
+ await browser.close();
+ }
+
+ res.status(500).json({
+ success: false,
+ error: 'Erreur lors de la génération du PDF'
+ });
+ }
+});
+
+// Fonction utilitaire pour échapper le HTML
+function escapeHtml(text) {
+ const map = {
+ '&': '&',
+ '<': '<',
+ '>': '>',
+ '"': '"',
+ "'": '''
+ };
+ return text.replace(/[&<>"']/g, function(m) { return map[m]; });
+}
+
+// Fonction pour convertir le markdown en HTML stylé avec support Mermaid
+function generateStyledHTML(content, title) {
+ let html = content;
+
+ // Traitement des diagrammes Mermaid AVANT les blocs de code
+ html = html.replace(/```mermaid\n([\s\S]*?)```/g, '$1
');
+
+ // Traitement des blocs de code avec langage
+ html = html.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => {
+ const language = lang ? ` class="language-${lang}"` : '';
+ return `${escapeHtml(code)}
`;
+ });
+
+ // Code en ligne
+ html = html.replace(/`([^`]+)`/g, '$1
');
+
+ // Traitement des tableaux amélioré
+ html = html.replace(/(\|.*\|\s*\n)+/g, (match) => {
+ const lines = match.trim().split('\n').filter(line => line.trim());
+ let tableHtml = '\n';
+ let inHeader = true;
+
+ lines.forEach((line, index) => {
+ // Ignorer les lignes de séparation
+ 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 += ' \n';
+ cells.forEach(cell => {
+ tableHtml += ` <${tag}>${cell}${tag}>\n`;
+ });
+ tableHtml += ' \n';
+
+ if (inHeader && index === 0) inHeader = false;
+ });
+
+ tableHtml += '
\n';
+ return tableHtml;
+ });
+
+ // Conversion markdown vers HTML
+ html = html
+ // Titres (en ordre décroissant)
+ .replace(/^#{6}\s+(.*$)/gm, '$1 ')
+ .replace(/^#{5}\s+(.*$)/gm, '$1 ')
+ .replace(/^#{4}\s+(.*$)/gm, '$1 ')
+ .replace(/^#{3}\s+(.*$)/gm, '$1 ')
+ .replace(/^#{2}\s+(.*$)/gm, '$1 ')
+ .replace(/^#{1}\s+(.*$)/gm, '$1 ')
+ // Citations
+ .replace(/^>\s+(.*$)/gm, '$1 ')
+ // Gras et italique (ordre important)
+ .replace(/\*\*\*(.*?)\*\*\*/g, '$1 ')
+ .replace(/\*\*(.*?)\*\*/g, '$1 ')
+ .replace(/\*(.*?)\*/g, '$1 ')
+ // Barré
+ .replace(/~~(.*?)~~/g, '$1')
+ // Listes numérotées
+ .replace(/^(\s*)(\d+)\. (.*$)/gm, '$3 ')
+ // Listes à puces et tâches
+ .replace(/^(\s*)- \[x\] (.*$)/gm, '✅ $2 ')
+ .replace(/^(\s*)- \[ \] (.*$)/gm, '☐ $2 ')
+ .replace(/^(\s*)[*\-+] (.*$)/gm, '$2 ')
+ // Liens
+ .replace(/\[([^\]]+)\]\(([^\)]+)\)/g, '$1 ')
+ // Ligne horizontale
+ .replace(/^---+$/gm, ' ');
+
+ // Grouper les listes consécutives
+ html = html.replace(/(]*>.*?<\/li>\s*)+/gs, (match) => {
+ if (match.includes('class="numbered"')) {
+ return '' + match.replace(/ class="numbered"/g, '') + ' ';
+ } else {
+ return '';
+ }
+ });
+
+ // Nettoyer les listes multiples consécutives
+ html = html.replace(/(<\/[uo]l>\s*<[uo]l>)/g, '');
+
+ // Grouper les citations consécutives
+ html = html.replace(/(.*?<\/blockquote>\s*)+/gs, (match) => {
+ const content = match.replace(/<\/?blockquote>/g, '');
+ return '' + content + ' ';
+ });
+
+ // Traiter les paragraphes
+ html = html.split('\n\n').map(paragraph => {
+ paragraph = paragraph.trim();
+ if (!paragraph) return '';
+
+ // Ne pas entourer les éléments de bloc dans des paragraphes
+ if (paragraph.match(/^<(h[1-6]|div|table|ul|ol|blockquote|hr|pre)/)) {
+ return paragraph;
+ }
+
+ return '' + paragraph.replace(/\n/g, ' ') + '
';
+ }).join('\n\n');
+
+ const styledHTML = `
+
+
+
+
+
+ ${title}
+
+
+
+
+
+ ${html}
+
+
+ `;
+
+ return styledHTML;
+}
+
+// Fonction pour nettoyer le nom de fichier
+function sanitizeFilename(filename) {
+ return filename
+ .replace(/[^\w\s-]/g, '') // Supprimer caractères spéciaux
+ .replace(/\s+/g, '-') // Remplacer espaces par tirets
+ .toLowerCase() // Minuscules
+ .substring(0, 100); // Limiter la longueur
+}
+
+module.exports = router;
\ No newline at end of file
diff --git a/routes/index.js b/routes/index.js
index 460e0c2..0bf3466 100644
--- a/routes/index.js
+++ b/routes/index.js
@@ -9,6 +9,9 @@ router.get('/', (req, res) => {
// Route à propos
router.get('/about', (req, res) => {
+ const { getHeader } = require('../views/header');
+ const { getFooter } = require('../views/footer');
+
res.send(`
@@ -19,31 +22,60 @@ router.get('/about', (req, res) => {
- Journal de Conception
-
- Cette application aide les équipes à réaliser un suivi structuré et collaboratif de la conception de leurs projets. Elle permet d’archiver les étapes clés, d’assurer la traçabilité des décisions, et de simplifier la coordination.
-
- Historique
-
- Ce projet est né du besoin de centraliser et d’organiser les notes de conception lors du développement de projets techniques.
-
- Équipe
-
- Augustin ROUX – Développeur et Concepteur principal
-
- Contact
-
- Email : augustin.j.l.roux@gmail.com
- Dépôt Git : https://gitea.legion-muyue.fr/Muyue/conception-assistant
-
-
+ ${getHeader()}
+
+
+
+ À propos de l'application
+
+
+ Cette application aide les équipes à réaliser un suivi structuré et collaboratif de la conception de leurs projets. Elle permet d'archiver les étapes clés, d'assurer la traçabilité des décisions, et de simplifier la coordination.
+
+
+
+
+
+ Historique
+
+
+ Ce projet est né du besoin de centraliser et d'organiser les notes de conception lors du développement de projets techniques. Il offre un environnement intuitif pour documenter les décisions architecturales et suivre l'évolution des projets.
+
+
+
+
+
+ Équipe
+
+
+
+ Augustin ROUX – Développeur et Concepteur principal
+
+
+
+
+
+
+
+
+
+
+ ${getFooter()}
+
+