🔧 Améliorations navigation et historique

- Scroll précis vers sections table des matières avec surbrillance
- Historique Ctrl+Z/Y fonctionnel pour toutes les actions IA
- Mode édition automatique lors du chargement de fichiers
- Support CSS GitHub Preview pour visualisation Markdown

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Augustin 2025-09-26 10:52:23 +02:00
parent a539adb6d1
commit b85e6d3a25
3 changed files with 675 additions and 73 deletions

View File

@ -0,0 +1,454 @@
/* CSS pour la prévisualisation du journal - Style GitHub authentique */
.markdown-preview {
/* Variables spécifiques GitHub */
--github-text-color: #1f2328;
--github-text-light: #656d76;
--github-bg-color: #ffffff;
--github-border-color: #d0d7de;
--github-border-muted: #d8dee4;
--github-accent-emphasis: #0969da;
--github-accent-fg: #0969da;
--github-neutral-muted: #afb8c1;
--github-canvas-subtle: #f6f8fa;
--github-danger-fg: #cf222e;
--github-success-fg: #1a7f37;
--github-attention-fg: #9a6700;
--github-severe-fg: #bc4c00;
--github-done-fg: #8250df;
/* Override pour thème sombre */
color: var(--github-text-color);
background: var(--github-bg-color);
border: 1px solid var(--github-border-color);
border-radius: 6px;
/* GitHub typography */
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
font-size: 16px;
line-height: 1.5;
word-wrap: break-word;
/* Layout comme GitHub */
max-width: none;
width: 100%;
padding: 48px;
margin: 0;
box-sizing: border-box;
}
/* Mode sombre GitHub */
body.dark-theme .markdown-preview {
--github-text-color: #e6edf3;
--github-text-light: #7d8590;
--github-bg-color: #0d1117;
--github-border-color: #30363d;
--github-border-muted: #21262d;
--github-accent-emphasis: #1f6feb;
--github-accent-fg: #58a6ff;
--github-neutral-muted: #6e7681;
--github-canvas-subtle: #161b22;
--github-danger-fg: #f85149;
--github-success-fg: #3fb950;
--github-attention-fg: #d29922;
--github-severe-fg: #db6d28;
--github-done-fg: #a5a3ff;
background: var(--github-bg-color);
color: var(--github-text-color);
border-color: var(--github-border-color);
}
/* Titres style GitHub */
.markdown-preview h1,
.markdown-preview h2,
.markdown-preview h3,
.markdown-preview h4,
.markdown-preview h5,
.markdown-preview h6 {
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
line-height: 1.25;
color: var(--github-text-color);
}
.markdown-preview h1 {
font-size: 2em;
border-bottom: 1px solid var(--github-border-muted);
padding-bottom: 0.3em;
margin-top: 0;
}
.markdown-preview h2 {
font-size: 1.5em;
border-bottom: 1px solid var(--github-border-muted);
padding-bottom: 0.3em;
}
.markdown-preview h3 {
font-size: 1.25em;
}
.markdown-preview h4 {
font-size: 1em;
}
.markdown-preview h5 {
font-size: 0.875em;
}
.markdown-preview h6 {
font-size: 0.85em;
color: var(--github-text-light);
}
/* Paragraphes */
.markdown-preview p {
margin-top: 0;
margin-bottom: 16px;
}
/* Listes style GitHub */
.markdown-preview ul,
.markdown-preview ol {
margin-top: 0;
margin-bottom: 16px;
padding-left: 2em;
}
.markdown-preview li {
word-wrap: break-all;
margin-bottom: 0;
}
.markdown-preview li > p {
margin-top: 0;
margin-bottom: 0;
}
.markdown-preview li + li {
margin-top: 0.25em;
}
/* Listes de tâches */
.markdown-preview .task-list-item {
list-style-type: none;
}
.markdown-preview .task-list-item input[type="checkbox"] {
margin: 0 0.2em 0.25em -1.4em;
vertical-align: middle;
}
/* Citations */
.markdown-preview blockquote {
margin: 0 0 16px 0;
padding: 0 1em;
color: var(--github-text-light);
border-left: 0.25em solid var(--github-border-color);
}
.markdown-preview blockquote > :first-child {
margin-top: 0;
}
.markdown-preview blockquote > :last-child {
margin-bottom: 0;
}
/* Code inline */
.markdown-preview code {
padding: 0.2em 0.4em;
margin: 0;
font-size: 85%;
white-space: break-spaces;
background-color: var(--github-neutral-muted);
border-radius: 6px;
font-family: ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace;
}
body.dark-theme .markdown-preview code {
background-color: rgba(110, 118, 129, 0.4);
}
/* Blocs de code */
.markdown-preview pre {
padding: 16px;
margin-bottom: 16px;
background-color: var(--github-canvas-subtle);
border-radius: 6px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
word-wrap: normal;
}
body.dark-theme .markdown-preview pre {
background-color: var(--github-canvas-subtle);
}
.markdown-preview pre code {
display: inline;
max-width: auto;
padding: 0;
margin: 0;
overflow: visible;
line-height: inherit;
word-wrap: normal;
background-color: transparent;
border: 0;
white-space: pre;
word-break: normal;
word-spacing: normal;
tab-size: 4;
}
/* Tables style GitHub */
.markdown-preview table {
border-spacing: 0;
border-collapse: collapse;
display: table;
width: 100%;
overflow: auto;
margin-top: 0;
margin-bottom: 16px;
border: 1px solid var(--github-border-color);
}
.markdown-preview table th {
font-weight: 600;
background-color: var(--github-canvas-subtle);
border: 1px solid var(--github-border-color);
padding: 6px 13px;
text-align: left;
vertical-align: top;
}
body.dark-theme .markdown-preview table th {
background-color: var(--github-canvas-subtle);
}
.markdown-preview table td {
border: 1px solid var(--github-border-color);
padding: 6px 13px;
text-align: left;
vertical-align: top;
}
.markdown-preview table tr {
background-color: var(--github-bg-color);
border-top: 1px solid var(--github-border-color);
}
.markdown-preview table tr:nth-child(even) {
background-color: var(--github-canvas-subtle);
}
body.dark-theme .markdown-preview table tr:nth-child(even) {
background-color: var(--github-canvas-subtle);
}
.markdown-preview table thead tr {
background-color: var(--github-canvas-subtle);
}
body.dark-theme .markdown-preview table thead tr {
background-color: var(--github-canvas-subtle);
}
/* Liens style GitHub */
.markdown-preview a {
color: var(--github-accent-fg);
text-decoration: none;
}
.markdown-preview a:hover {
text-decoration: underline;
}
.markdown-preview a:visited {
color: var(--github-done-fg);
}
/* Images */
.markdown-preview img {
max-width: 100%;
box-sizing: content-box;
background-color: var(--github-bg-color);
}
.markdown-preview img[align=right] {
padding-left: 20px;
}
.markdown-preview img[align=left] {
padding-right: 20px;
}
/* Séparateurs horizontaux */
.markdown-preview hr {
height: 0.25em;
padding: 0;
margin: 24px 0;
background-color: var(--github-border-color);
border: 0;
border-radius: 6px;
}
/* Emphasis */
.markdown-preview strong {
font-weight: 600;
}
.markdown-preview em {
font-style: italic;
}
/* Strikethrough */
.markdown-preview del {
text-decoration: line-through;
}
/* Keyboard keys */
.markdown-preview kbd {
display: inline-block;
padding: 3px 5px;
font-size: 11px;
line-height: 10px;
color: var(--github-text-light);
vertical-align: middle;
background-color: var(--github-canvas-subtle);
border: solid 1px var(--github-neutral-muted);
border-bottom-color: var(--github-neutral-muted);
border-radius: 6px;
box-shadow: inset 0 -1px 0 var(--github-neutral-muted);
font-family: ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace;
}
/* Support pour les alertes GitHub (Notes, Tips, etc.) */
.markdown-preview .markdown-alert {
padding: 0.5rem 1em;
margin-bottom: 16px;
color: inherit;
border-left: 0.25em solid var(--github-border-color);
background-color: var(--github-canvas-subtle);
border-radius: 0 6px 6px 0;
}
.markdown-preview .markdown-alert > :first-child {
margin-top: 0;
}
.markdown-preview .markdown-alert > :last-child {
margin-bottom: 0;
}
.markdown-preview .markdown-alert .markdown-alert-title {
display: flex;
font-weight: 500;
align-items: center;
line-height: 1;
}
.markdown-preview .markdown-alert.markdown-alert-note {
border-left-color: var(--github-accent-emphasis);
}
.markdown-preview .markdown-alert.markdown-alert-tip {
border-left-color: var(--github-success-fg);
}
.markdown-preview .markdown-alert.markdown-alert-important {
border-left-color: var(--github-done-fg);
}
.markdown-preview .markdown-alert.markdown-alert-warning {
border-left-color: var(--github-attention-fg);
}
.markdown-preview .markdown-alert.markdown-alert-caution {
border-left-color: var(--github-danger-fg);
}
/* Support pour Mermaid avec style GitHub */
.markdown-preview .mermaid {
text-align: center;
margin: 16px 0;
background: transparent;
}
.markdown-preview .mermaid svg {
max-width: 100%;
height: auto;
}
/* Support pour les footnotes */
.markdown-preview .footnotes {
font-size: 0.875em;
color: var(--github-text-light);
border-top: 1px solid var(--github-border-color);
margin-top: 24px;
padding-top: 24px;
}
.markdown-preview .footnotes ol {
padding-left: 16px;
}
.markdown-preview .footnotes li {
margin: 0.25rem 0;
}
/* Amélioration de l'espacement pour les éléments imbriqués */
.markdown-preview li p {
margin-bottom: 0;
margin-top: 0;
}
.markdown-preview li blockquote {
margin: 8px 0;
}
.markdown-preview li ul,
.markdown-preview li ol {
margin-top: 0;
margin-bottom: 0;
}
.markdown-preview ul ul,
.markdown-preview ul ol,
.markdown-preview ol ol,
.markdown-preview ol ul {
margin-top: 0;
margin-bottom: 0;
}
/* Scrollbars GitHub-like (webkit only) */
.markdown-preview::-webkit-scrollbar {
width: 16px;
height: 16px;
}
.markdown-preview::-webkit-scrollbar-corner,
.markdown-preview::-webkit-scrollbar-track {
background-color: transparent;
}
.markdown-preview::-webkit-scrollbar-thumb {
background-color: var(--github-neutral-muted);
border-radius: 8px;
border: 4px solid transparent;
background-clip: content-box;
}
.markdown-preview::-webkit-scrollbar-thumb:hover {
background-color: var(--github-text-light);
}
/* Responsive pour mobile */
@media (max-width: 768px) {
.markdown-preview {
padding: 24px 16px;
}
}

View File

@ -125,13 +125,24 @@ class ConceptionAssistant {
this.showNotification('Retour en haut', 'success'); this.showNotification('Retour en haut', 'success');
} }
saveState() { saveState(immediate = false) {
// Si immediate est true, sauvegarder immédiatement sans debounce
if (immediate) {
this.performSaveState();
return;
}
// Éviter de sauvegarder trop souvent (debounce) // Éviter de sauvegarder trop souvent (debounce)
if (this.saveStateTimer) { if (this.saveStateTimer) {
clearTimeout(this.saveStateTimer); clearTimeout(this.saveStateTimer);
} }
this.saveStateTimer = setTimeout(() => { this.saveStateTimer = setTimeout(() => {
this.performSaveState();
}, 1000); // Sauvegarder après 1 seconde d'inactivité
}
performSaveState() {
const currentContent = this.editor.innerText; const currentContent = this.editor.innerText;
// Ne pas sauvegarder si le contenu n'a pas changé // Ne pas sauvegarder si le contenu n'a pas changé
@ -148,7 +159,6 @@ class ConceptionAssistant {
// Vider la pile de refaire car on a fait une nouvelle action // Vider la pile de refaire car on a fait une nouvelle action
this.redoStack = []; this.redoStack = [];
}, 1000); // Sauvegarder après 1 seconde d'inactivité
} }
undo() { undo() {
@ -262,6 +272,14 @@ class ConceptionAssistant {
this.currentJournalId = id; this.currentJournalId = id;
this.editor.innerText = journal.markdownContent; this.editor.innerText = journal.markdownContent;
this.generateTOC(); this.generateTOC();
// Réinitialiser l'historique pour le nouveau journal
this.undoStack = [journal.markdownContent];
this.redoStack = [];
// S'assurer que l'éditeur est en mode édition
this.ensureEditMode();
this.showNotification('Journal chargé', 'success'); this.showNotification('Journal chargé', 'success');
} }
} catch (error) { } catch (error) {
@ -336,6 +354,14 @@ class ConceptionAssistant {
this.editor.innerText = ''; this.editor.innerText = '';
this.generateTOC(); this.generateTOC();
this.clearFeedback(); this.clearFeedback();
// Réinitialiser l'historique pour le nouveau journal
this.undoStack = [''];
this.redoStack = [];
// S'assurer que l'éditeur est en mode édition
this.ensureEditMode();
this.showNotification('Nouveau journal créé', 'success'); this.showNotification('Nouveau journal créé', 'success');
} }
@ -441,64 +467,134 @@ class ConceptionAssistant {
} }
scrollToHeading(title) { scrollToHeading(title) {
try {
// Méthode plus simple et robuste : chercher le texte directement dans l'éditeur
const content = this.editor.innerText; const content = this.editor.innerText;
const lines = content.split('\n'); const lines = content.split('\n');
// Chercher la ligne qui correspond au titre // Trouver l'index de la ligne correspondant au titre
let targetLineIndex = -1;
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim(); const line = lines[i].trim();
if (line.startsWith('#') && line.replace(/^#+\s*/, '') === title) { if (line.startsWith('#') && line.replace(/^#+\s*/, '') === title) {
try { targetLineIndex = i;
// Approche plus simple : utiliser un élément temporaire avec une position spécifique break;
const tempElement = document.createElement('div'); }
tempElement.style.position = 'absolute'; }
tempElement.style.visibility = 'hidden';
tempElement.style.height = '1px';
// Calculer la position approximative en fonction de la ligne if (targetLineIndex === -1) {
const editorRect = this.editor.getBoundingClientRect(); this.showNotification('Section non trouvée', 'warning');
const computedStyle = window.getComputedStyle(this.editor); return;
const lineHeight = parseInt(computedStyle.lineHeight) || 20; }
// Insérer l'élément temporaire dans l'éditeur // Calculer la position approximative de la ligne
this.editor.appendChild(tempElement); const editorStyles = window.getComputedStyle(this.editor);
const lineHeight = parseFloat(editorStyles.lineHeight) || 20;
const paddingTop = parseFloat(editorStyles.paddingTop) || 0;
// Calculer la position de scroll basée sur le numéro de ligne // Calculer la position de scroll basée sur le numéro de ligne
const targetPosition = i * lineHeight; const targetScrollPosition = (targetLineIndex * lineHeight) + paddingTop;
// Scroller vers la position calculée // Déterminer si l'éditeur ou la fenêtre doit être scrollée
const editorRect = this.editor.getBoundingClientRect();
const editorHasScroll = this.editor.scrollHeight > this.editor.clientHeight;
if (editorHasScroll) {
// Scroller dans l'éditeur
this.editor.scrollTo({ this.editor.scrollTo({
top: targetPosition, top: Math.max(0, targetScrollPosition - 60),
behavior: 'smooth' behavior: 'smooth'
}); });
} else {
// Si l'éditeur n'a pas de scroll, utiliser le parent // Scroller la page entière
if (this.editor.scrollHeight <= this.editor.clientHeight) {
const scrollContainer = this.editor.parentElement || document.documentElement;
const editorTop = this.editor.offsetTop; const editorTop = this.editor.offsetTop;
const windowScrollTarget = editorTop + targetScrollPosition - 100;
scrollContainer.scrollTo({ window.scrollTo({
top: editorTop + targetPosition - 100, // -100 pour un padding top: Math.max(0, windowScrollTarget),
behavior: 'smooth' behavior: 'smooth'
}); });
} }
// Nettoyer l'élément temporaire // Optionnel : mettre en surbrillance temporairement le titre
setTimeout(() => { this.highlightHeading(title);
if (tempElement.parentNode) {
tempElement.parentNode.removeChild(tempElement); this.showNotification(`Navigation vers: ${title}`, 'success');
}
}, 100);
this.showNotification('Navigation vers la section', 'success');
return;
} catch (error) { } catch (error) {
console.log('Erreur de scroll:', error); console.error('Erreur de scroll:', error);
} this.showNotification('Erreur lors de la navigation', 'error');
} }
} }
this.showNotification('Section non trouvée', 'warning'); highlightHeading(title) {
// Fonction pour mettre en surbrillance temporairement le titre trouvé
try {
const content = this.editor.innerText;
const lines = content.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line.startsWith('#') && line.replace(/^#+\s*/, '') === title) {
// Créer un range pour sélectionner la ligne
const selection = window.getSelection();
const range = document.createRange();
// Trouver le nœud texte et la position
const walker = document.createTreeWalker(
this.editor,
NodeFilter.SHOW_TEXT,
null,
false
);
let currentLine = 0;
let textNode = walker.nextNode();
while (textNode && currentLine < i) {
const nodeText = textNode.textContent;
const newLines = (nodeText.match(/\n/g) || []).length;
currentLine += newLines;
if (currentLine < i) {
textNode = walker.nextNode();
}
}
if (textNode) {
// Trouver le début de la ligne dans ce nœud
const nodeText = textNode.textContent;
const linesInNode = nodeText.split('\n');
const targetLineInNode = i - (currentLine - linesInNode.length + 1);
if (targetLineInNode >= 0 && targetLineInNode < linesInNode.length) {
let startPos = 0;
for (let j = 0; j < targetLineInNode; j++) {
startPos += linesInNode[j].length + 1;
}
const endPos = startPos + linesInNode[targetLineInNode].length;
range.setStart(textNode, startPos);
range.setEnd(textNode, endPos);
// Sélectionner temporairement
selection.removeAllRanges();
selection.addRange(range);
// Retirer la sélection après un court délai
setTimeout(() => {
selection.removeAllRanges();
}, 1000);
}
}
break;
}
}
} catch (error) {
// Ignorer les erreurs de surbrillance, ce n'est pas critique
console.log('Erreur surbrillance:', error);
}
} }
exportMarkdown() { exportMarkdown() {
@ -529,9 +625,18 @@ class ConceptionAssistant {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (e) => { reader.onload = (e) => {
this.editor.innerText = e.target.result; const importedContent = e.target.result;
this.editor.innerText = importedContent;
this.generateTOC(); this.generateTOC();
this.currentJournalId = null; // Nouveau journal this.currentJournalId = null; // Nouveau journal
// Réinitialiser l'historique pour le fichier importé
this.undoStack = [importedContent];
this.redoStack = [];
// S'assurer que l'éditeur est en mode édition
this.ensureEditMode();
this.showNotification('Fichier Markdown importé', 'success'); this.showNotification('Fichier Markdown importé', 'success');
}; };
reader.readAsText(file); reader.readAsText(file);
@ -633,6 +738,9 @@ class ConceptionAssistant {
break; break;
case 'liberty': case 'liberty':
// Sauvegarder l'état avant les modifications de l'IA
this.saveState(true);
const count = document.getElementById('liberty-repeat-count')?.value || 3; const count = document.getElementById('liberty-repeat-count')?.value || 3;
// Mode liberté utilise toujours le document complet // Mode liberté utilise toujours le document complet
result = await this.callAI('/api/ai/liberty-mode', { content: fullContent, iterations: count, focus: 'conception' }); result = await this.callAI('/api/ai/liberty-mode', { content: fullContent, iterations: count, focus: 'conception' });
@ -641,7 +749,8 @@ class ConceptionAssistant {
if (result.finalContent) { if (result.finalContent) {
this.editor.innerText = result.finalContent; this.editor.innerText = result.finalContent;
this.generateTOC(); this.generateTOC();
this.saveState(); // Sauvegarder l'état après les modifications de l'IA
this.saveState(true);
} }
let libertyHTML = `<strong>Mode Liberté Intelligent (${result.iterations} itérations)</strong><br><br>`; let libertyHTML = `<strong>Mode Liberté Intelligent (${result.iterations} itérations)</strong><br><br>`;
@ -775,6 +884,9 @@ class ConceptionAssistant {
if (!this.lastRephraseData) return; if (!this.lastRephraseData) return;
try { try {
// Sauvegarder l'état avant la reformulation pour permettre l'undo
this.saveState(true);
// Remplacer le texte dans l'éditeur // Remplacer le texte dans l'éditeur
const range = this.lastRephraseData.selection.getRangeAt(0); const range = this.lastRephraseData.selection.getRangeAt(0);
range.deleteContents(); range.deleteContents();
@ -784,6 +896,9 @@ class ConceptionAssistant {
window.getSelection().removeAllRanges(); window.getSelection().removeAllRanges();
this.generateTOC(); this.generateTOC();
// Sauvegarder l'état après la reformulation
this.saveState(true);
// Afficher un message de succès // Afficher un message de succès
this.showNotification('Reformulation appliquée avec succès', 'success'); this.showNotification('Reformulation appliquée avec succès', 'success');
this.clearFeedback(); this.clearFeedback();
@ -806,6 +921,31 @@ class ConceptionAssistant {
`; `;
} }
ensureEditMode() {
// Si on est en mode preview, forcer le retour en mode édition
if (this.isPreviewMode) {
const previewBtn = document.getElementById('preview-toggle');
// Revenir en mode édition sans utiliser originalContent car on veut le nouveau contenu
this.editor.contentEditable = true;
this.editor.style.background = '';
this.editor.style.border = '';
this.editor.style.borderRadius = '';
// Changer le bouton
if (previewBtn) {
previewBtn.innerHTML = 'Visualiser';
previewBtn.classList.remove('secondary');
previewBtn.classList.add('primary');
}
this.isPreviewMode = false;
}
// S'assurer que l'éditeur est toujours éditable
this.editor.contentEditable = true;
}
showNotification(message, type = 'success') { showNotification(message, type = 'success') {
const notification = document.createElement('div'); const notification = document.createElement('div');
notification.className = `notification ${type}`; notification.className = `notification ${type}`;
@ -1059,6 +1199,13 @@ function initializeTemplateForm() {
app.generateTOC(); app.generateTOC();
app.currentJournalId = null; // Nouveau journal app.currentJournalId = null; // Nouveau journal
// Réinitialiser l'historique pour le nouveau template
app.undoStack = [result.data.content];
app.redoStack = [];
// S'assurer que l'éditeur est en mode édition
app.ensureEditMode();
app.showNotification(`Template ${domain}/${level} chargé avec succès`, 'success'); app.showNotification(`Template ${domain}/${level} chargé avec succès`, 'success');
closeAllPanels(); closeAllPanels();
} else { } else {

View File

@ -11,6 +11,7 @@ function getHead(){
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Conception-Assistant</title> <title>Conception-Assistant</title>
<link rel="stylesheet" href="/assets/css/style.css"> <link rel="stylesheet" href="/assets/css/style.css">
<link rel="stylesheet" href="/assets/css/github-preview.css">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📝</text></svg>"> <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📝</text></svg>">
<!-- Marked.js pour le rendu Markdown compatible GitHub --> <!-- Marked.js pour le rendu Markdown compatible GitHub -->
<script src="https://cdn.jsdelivr.net/npm/marked@9.1.6/marked.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/marked@9.1.6/marked.min.js"></script>