diff --git a/.next/dev/cache/turbopack/23c46498/CURRENT b/.next/dev/cache/turbopack/23c46498/CURRENT index 4a1cf43..dedfcdf 100644 Binary files a/.next/dev/cache/turbopack/23c46498/CURRENT and b/.next/dev/cache/turbopack/23c46498/CURRENT differ diff --git a/.next/dev/cache/turbopack/23c46498/LOG b/.next/dev/cache/turbopack/23c46498/LOG index 0d91c6b..ef4a4ee 100644 --- a/.next/dev/cache/turbopack/23c46498/LOG +++ b/.next/dev/cache/turbopack/23c46498/LOG @@ -5790,3 +5790,15 @@ FAM | META SEQ | SST SEQ | RANGE 0 | 00012935 | 00012934 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh) 1 | 00012936 | 00012932 SST | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1 (0 MiB, fresh) 2 | 00012937 | 00012933 SST | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1 (0 MiB, fresh) +Time 2026-03-05T07:09:14.1907086Z +Commit 00013255 4 keys in 8ms 671µs +FAM | META SEQ | SST SEQ | RANGE + 0 | 00013253 | 00013252 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh) + 1 | 00013254 | 00013250 SST | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1 (0 MiB, fresh) + 2 | 00013255 | 00013251 SST | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1 (0 MiB, fresh) +Time 2026-03-05T07:09:26.7824463Z +Commit 00013261 4 keys in 7ms 646µs 800ns +FAM | META SEQ | SST SEQ | RANGE + 0 | 00013259 | 00013258 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh) + 1 | 00013260 | 00013256 SST | O | b294a4237ccef201-b294a4237ccef201 (0 MiB, fresh) + 2 | 00013261 | 00013257 SST | O | b294a4237ccef201-b294a4237ccef201 (0 MiB, fresh) diff --git a/nixpacks.toml b/nixpacks.toml index 329f197..651090a 100644 --- a/nixpacks.toml +++ b/nixpacks.toml @@ -1,10 +1,19 @@ +[phases.setup] +nixPkgs = ["nodejs_22", "openssl"] + +[phases.install] +cmds = ["npm install"] + [phases.build] cmds = [ "npm run build", - "mkdir -p .next/standalone/public", - "cp -r public/* .next/standalone/public/ || true", - "cp -r .next/static .next/standalone/.next/static" + # On prépare le standalone avec ses assets + "cp -r public .next/standalone/public", + "cp -r .next/static .next/standalone/.next/static", + # NETTOYAGE RADICAL : on supprime tout sauf le standalone + "find . -maxdepth 1 ! -name '.next' ! -name '.' -exec rm -rf {} +" ] -[phases.setup] -nixPkgs = ["nodejs_22", "npm-9_x", "openssl"] \ No newline at end of file +[start] +# On lance le serveur depuis le dossier standalone +cmd = "node .next/standalone/server.js" \ No newline at end of file diff --git a/src/components/RichTextEditor.tsx b/src/components/RichTextEditor.tsx index c7c5e86..c6c8d87 100644 --- a/src/components/RichTextEditor.tsx +++ b/src/components/RichTextEditor.tsx @@ -168,18 +168,20 @@ const RichTextEditor = forwardRef(({ useEffect(() => { if (!contentRef.current || initialContent === undefined) return; - // Ignore exact loopbacks from our own saves - if (initialContent === syncRef.current) return; + // 1. Si le contenu entrant est identique à ce qu'on a déjà, on ne touche à rien + if (initialContent === contentRef.current.innerHTML) return; - // Safety: never overwrite real content with an empty string from a stale/placeholder source - const hasRealContent = latestContentRef.current && latestContentRef.current.trim().length > 0; - if (!initialContent && hasRealContent) return; + // 2. LOGIQUE CRUCIALE : On ne met à jour le DOM que si : + // - L'éditeur est vide (premier chargement) + // - OU le document a changé (si vous gérez des IDs de documents) + // - OU si l'utilisateur n'est PAS en train de focus l'éditeur + const isUserEditing = document.activeElement === contentRef.current; - // We reached here, so initialContent is genuinely NEW data we didn't know about. - // E.g. clicked another chapter, or data was modified in another tab/device. - contentRef.current.innerHTML = initialContent; - syncRef.current = initialContent; - latestContentRef.current = initialContent; + if (!isUserEditing || (contentRef.current.innerHTML === "" && initialContent !== "")) { + contentRef.current.innerHTML = initialContent; + syncRef.current = initialContent; + latestContentRef.current = initialContent; + } }, [initialContent]); // Flush pending save on unmount @@ -187,9 +189,11 @@ const RichTextEditor = forwardRef(({ return () => { if (saveTimeoutRef.current) { clearTimeout(saveTimeoutRef.current); - if (latestContentRef.current !== syncRef.current && onSave) { - onSave(latestContentRef.current); - } + } + // Always save if there are unsaved changes, regardless of timer + if (latestContentRef.current !== syncRef.current && onSave) { + syncRef.current = latestContentRef.current; + onSave(latestContentRef.current); } }; }, [onSave]); @@ -217,8 +221,15 @@ const RichTextEditor = forwardRef(({ saveTimeoutRef.current = setTimeout(async () => { setSaveStatus('saving'); const htmlToSave = latestContentRef.current; - await onSave(htmlToSave); - syncRef.current = htmlToSave; // Record that we've synced this exact string to the server + // Update syncRef BEFORE calling onSave, because onSave triggers setProjects + // which causes a re-render. The useEffect must see the updated syncRef + // to avoid re-writing innerHTML unnecessarily. + syncRef.current = htmlToSave; + try { + await onSave(htmlToSave); + } catch (err) { + console.error('Auto-save failed:', err); + } setSaveStatus('saved'); }, 2000); // 2 seconds } diff --git a/src/components/layout/EditorShell.tsx b/src/components/layout/EditorShell.tsx index 74e2877..dff8c2d 100644 --- a/src/components/layout/EditorShell.tsx +++ b/src/components/layout/EditorShell.tsx @@ -4,7 +4,7 @@ import React, { useState, useEffect } from 'react'; import { BookProject, UserProfile, ViewMode, ChatMessage } from '@/lib/types'; import AIPanel from '@/components/AIPanel'; -import { Book, FileText, Globe, GitGraph, Lightbulb, Settings, Menu, ChevronRight, ChevronLeft, Share2, HelpCircle, LogOut, LayoutDashboard, User, Plus, Trash2 } from 'lucide-react'; +import { Book, FileText, Globe, GitGraph, Lightbulb, Settings, Menu, ChevronRight, ChevronLeft, Share2, HelpCircle, LogOut, LayoutDashboard, User, Plus, Trash2, Wand2 } from 'lucide-react'; import { useLanguage } from '@/providers/LanguageProvider'; import { LanguageSwitcher } from '@/components/LanguageSwitcher'; @@ -157,6 +157,18 @@ const EditorShell: React.FC = (props) => {
{isAiPanelOpen && }
+ + {/* Floating Mobile Button for AI Panel */} + {!isAiPanelOpen && ( + + )} ); };