/** * @license * SPDX-License-Identifier: Apache-2.0 */ import React, { useState, useMemo } from 'react'; import { motion, AnimatePresence } from 'motion/react'; import { X } from 'lucide-react'; // Components import { Sidebar } from './components/Sidebar'; import { NotebooksView } from './components/NotebooksView'; import { AgentsView } from './components/AgentsView'; import { SettingsView } from './components/SettingsView'; import { TrashView } from './components/TrashView'; import { BrainstormView } from './components/BrainstormView/BrainstormView'; import { InsightsView } from './components/InsightsView'; import { TemporalView } from './components/TemporalView'; import { AISidebar } from './components/AISidebar'; import { SlashMenu } from './components/SlashMenu'; import { LandingPage } from './components/LandingPage'; import { AuthPage } from './components/AuthPage'; import { SearchModal } from './components/SearchModal'; import { ClipperSimulator } from './components/ClipperSimulator'; import { GraphKnowledgeMap } from './components/GraphKnowledgeMap'; import { RevisionView } from './components/RevisionView'; // Data & Services import { CARNETS, ALL_NOTES } from './constants'; import { generateFlashcardsForNote } from './services/geminiService'; import { NavigationView, SettingsTab, AITab, AITone, Carnet, Note, BrainstormIdea, NoteAccessLog, Flashcard } from './types'; export default function App() { const [showLanding, setShowLanding] = useState(() => { // Check if user has already "entered" the app once in this session return !sessionStorage.getItem('momento-entered'); }); const [showAuth, setShowAuth] = useState(false); const [authMode, setAuthMode] = useState<'login' | 'register'>('login'); const [isAuthenticated, setIsAuthenticated] = useState(false); const [activeView, setActiveView] = useState('notebooks'); const [activeSettingsTab, setActiveSettingsTab] = useState('general'); const [selectedAgentId, setSelectedAgentId] = useState(null); const [isDarkMode, setIsDarkMode] = useState(false); const [carnets, setCarnets] = useState(CARNETS); const [notes, setNotes] = useState(ALL_NOTES); const [accessLogs, setAccessLogs] = useState([ // Note n1: 14-day cycle { noteId: 'n1', accessedAt: new Date(Date.now() - 70 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' }, { noteId: 'n1', accessedAt: new Date(Date.now() - 56 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' }, { noteId: 'n1', accessedAt: new Date(Date.now() - 42 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' }, { noteId: 'n1', accessedAt: new Date(Date.now() - 28 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' }, { noteId: 'n1', accessedAt: new Date(Date.now() - 14 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' }, { noteId: 'n1', accessedAt: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' }, // Note n2: 7-day cycle { noteId: 'n2', accessedAt: new Date(Date.now() - 35 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' }, { noteId: 'n2', accessedAt: new Date(Date.now() - 28 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' }, { noteId: 'n2', accessedAt: new Date(Date.now() - 21 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' }, { noteId: 'n2', accessedAt: new Date(Date.now() - 14 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' }, { noteId: 'n2', accessedAt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' }, { noteId: 'n2', accessedAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' }, // Note n3: 3-day cycle (frequent check) { noteId: 'n3', accessedAt: new Date(Date.now() - 12 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' }, { noteId: 'n3', accessedAt: new Date(Date.now() - 9 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' }, { noteId: 'n3', accessedAt: new Date(Date.now() - 6 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' }, { noteId: 'n3', accessedAt: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' }, { noteId: 'n3', accessedAt: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' }, ]); const logNoteAccess = (noteId: string, action: 'view' | 'edit' | 'search_hit' = 'view') => { const newLog: NoteAccessLog = { noteId, accessedAt: new Date().toISOString(), action }; setAccessLogs(prev => [...prev, newLog]); }; const [activeCarnetId, setActiveCarnetId] = useState('4'); const [activeNoteId, setActiveNoteId] = useState(null); const [brainstormSeed, setBrainstormSeed] = useState(null); const [accentColor, setAccentColor] = useState(() => { return localStorage.getItem('momento-accent-color') || '#A47148'; }); // Flashcards state with beautiful architectural starter deck const [flashcards, setFlashcards] = useState(() => { const stored = localStorage.getItem('momento-flashcards'); if (stored) { try { return JSON.parse(stored); } catch (e) { console.error("Failed to parse stored flashcards:", e); } } // Beautiful default seeds for 'n1' (Grid Systems & Geometry) const SEEDS: Flashcard[] = [ { id: 'f1', noteId: 'n1', question: 'Quel est l’intérêt primordial des trames géométriques en conception spatiale ?', answer: 'Elles structurent l’espace bâti en créant un sens d\'ordre, de rythme, et d\'harmonie de proportions esthétiques dans l\'environnement, facilitant la lisibilité de la structure.', intervalDays: 1, nextReviewDate: new Date().toISOString(), // Due today easeFactor: 2.5, mastered: false }, { id: 'f2', noteId: 'n1', question: 'En quoi l’approche dynamique paramétrique déforme-t-elle les grilles de construction traditionnelles ?', answer: 'Par l\'utilisation d\'algorithmes mathématiques de déformation réactifs à des ensembles de données environnementales pour créer des géométries fluides mais structurellement ordonnées.', intervalDays: 3, nextReviewDate: new Date(Date.now() + 1000 * 60 * 60 * 24 * 3).toISOString(), // Due soon easeFactor: 2.5, mastered: true }, { id: 'f3', noteId: 'n1', question: 'Quelle est la particularité de l’intégration de la lumière comme matériau d’espace ?', answer: 'La soustraction du superflu permet aux reflets et à la diffraction lumineuse de créer des profondeurs visuelles changeantes sans surcharger l\'aménagement matériel.', intervalDays: 1, nextReviewDate: new Date().toISOString(), // Due today easeFactor: 2.4, mastered: false } ]; return SEEDS; }); const [isGeneratingFlashcards, setIsGeneratingFlashcards] = useState(false); const [activeReviewDeckId, setActiveReviewDeckId] = useState(null); const [toast, setToast] = useState<{ show: boolean; message: string }>({ show: false, message: '' }); React.useEffect(() => { document.documentElement.style.setProperty('--color-accent', accentColor); localStorage.setItem('momento-accent-color', accentColor); }, [accentColor]); const handleBrainstormNote = (note: Note) => { setActiveView('brainstorm'); // We'll use a small delay or a ref to pass this to BrainstormView if needed, // but better to just share state or use a CustomEvent window.dispatchEvent(new CustomEvent('start-brainstorm', { detail: { seed: note.title, sourceNoteId: note.id } })); }; const handleGenerateFlashcards = async (noteId: string) => { const targetNote = notes.find(n => n.id === noteId); if (!targetNote) return; setIsGeneratingFlashcards(true); try { const rawContent = targetNote.content || ""; const generated = await generateFlashcardsForNote(targetNote.title, rawContent); if (generated && generated.length > 0) { const mappedCards: Flashcard[] = generated.map((c, i) => ({ id: `f-${noteId}-${Date.now()}-${i}`, noteId, question: c.question, answer: c.answer, intervalDays: 1, nextReviewDate: new Date().toISOString(), easeFactor: 2.5, mastered: false })); const updatedSet = [...flashcards.filter(fc => fc.noteId !== noteId), ...mappedCards]; setFlashcards(updatedSet); localStorage.setItem('momento-flashcards', JSON.stringify(updatedSet)); setToast({ show: true, message: `${mappedCards.length} flashcards créées avec succès pour "${targetNote.title}".` }); setTimeout(() => setToast(t => ({ ...t, show: false })), 4000); } else { setToast({ show: true, message: "Impossible d'extraire des flashcards exploitables à partir du texte existant." }); setTimeout(() => setToast(t => ({ ...t, show: false })), 4000); } } catch (err) { console.error("AI flashcards expansion failed:", err); setToast({ show: true, message: "Une erreur est survenue lors de la génération avec Gemini." }); setTimeout(() => setToast(t => ({ ...t, show: false })), 4000); } finally { setIsGeneratingFlashcards(false); } }; React.useEffect(() => { if (activeNoteId) { logNoteAccess(activeNoteId); } }, [activeNoteId]); React.useEffect(() => { // Check for session in URL const params = new URLSearchParams(window.location.search); const session = params.get('session'); if (session) { setActiveView('brainstorm'); // We pass it via a global property or custom event since BrainstormView will fetch sessions (window as any).initialSessionId = session; } const handleSwitchView = (e: any) => { if (e.detail) { setActiveView(e.detail as NavigationView); } }; const handleGlobalShortcut = (e: KeyboardEvent) => { // Trigger advanced search with Ctrl+F or Cmd+F or Ctrl+P or Cmd+P if ((e.ctrlKey || e.metaKey) && (e.key === 'f' || e.key === 'p')) { e.preventDefault(); setIsSearchOpen(true); } }; const handleToggleClipper = () => { setIsClipperSimulatorOpen(prev => !prev); }; window.addEventListener('switch-view', handleSwitchView); window.addEventListener('keydown', handleGlobalShortcut); window.addEventListener('toggle-clipper-simulator', handleToggleClipper); return () => { window.removeEventListener('switch-view', handleSwitchView); window.removeEventListener('keydown', handleGlobalShortcut); window.removeEventListener('toggle-clipper-simulator', handleToggleClipper); }; }, []); const [isSearchOpen, setIsSearchOpen] = useState(false); const [isClipperSimulatorOpen, setIsClipperSimulatorOpen] = useState(false); const [clipperToast, setClipperToast] = useState<{ id: string; title: string; noteId: string } | null>(null); const [selectedTagIds, setSelectedTagIds] = useState([]); const [isAISidebarOpen, setIsAISidebarOpen] = useState(false); const [isSidebarOpen, setIsSidebarOpen] = useState(false); const [aiTab, setAiTab] = useState('discussion'); const [selectedTone, setSelectedTone] = useState('Professional'); // Modal States const [showNewCarnetModal, setShowNewCarnetModal] = useState<{ isOpen: boolean; parentId?: string; isRenaming?: boolean; carnetId?: string }>({ isOpen: false }); const [showNewNoteModal, setShowNewNoteModal] = useState(false); const [slashMenu, setSlashMenu] = useState<{ isOpen: boolean; top: number; left: number } | null>(null); // Form States const [newCarnetName, setNewCarnetName] = useState(''); const [newNoteTitle, setNewNoteTitle] = useState(''); const [newNoteContent, setNewNoteContent] = useState(''); const handleEditorKeyDown = (e: React.KeyboardEvent) => { if (e.key === '/') { const selection = window.getSelection(); if (selection && selection.rangeCount > 0) { const range = selection.getRangeAt(0); const rect = range.getBoundingClientRect(); setSlashMenu({ isOpen: true, top: rect.bottom + window.scrollY, left: rect.left + window.scrollX }); } } }; React.useEffect(() => { if (clipperToast) { const timer = setTimeout(() => { setClipperToast(null); }, 4000); return () => clearTimeout(timer); } }, [clipperToast]); const handleOpenClippedNote = (noteId: string) => { setActiveNoteId(noteId); const found = notes.find(n => n.id === noteId); if (found) { setActiveCarnetId(found.carnetId); } setActiveView('notebooks'); }; const togglePin = (noteId: string) => { setNotes(notes.map(n => n.id === noteId ? { ...n, isPinned: !n.isPinned } : n)); }; const filteredNotes = useMemo(() => { let result = notes.filter(n => n.carnetId === activeCarnetId && !n.isDeleted); if (selectedTagIds.length > 0) { result = result.filter(note => selectedTagIds.every(tagId => note.tags?.some(tag => tag.id === tagId)) ); } return [...result].sort((a, b) => { if (a.isPinned && !b.isPinned) return -1; if (!a.isPinned && b.isPinned) return 1; return 0; }); }, [activeCarnetId, notes]); const activeNote = useMemo(() => notes.find(n => n.id === activeNoteId), [activeNoteId, notes]); const activeCarnet = useMemo(() => carnets.find(c => c.id === activeCarnetId), [activeCarnetId, carnets]); const handleAddCarnet = (e: React.FormEvent) => { e.preventDefault(); if (!newCarnetName.trim()) return; if (showNewCarnetModal.isRenaming && showNewCarnetModal.carnetId) { setCarnets(carnets.map(c => c.id === showNewCarnetModal.carnetId ? { ...c, name: newCarnetName, initial: newCarnetName.charAt(0).toUpperCase() } : c)); setShowNewCarnetModal({ isOpen: false }); setNewCarnetName(''); return; } const newCarnet: Carnet = { id: Date.now().toString(), name: newCarnetName, initial: newCarnetName.charAt(0).toUpperCase(), type: 'Project', parentId: showNewCarnetModal.parentId }; setCarnets([...carnets, newCarnet]); setNewCarnetName(''); setShowNewCarnetModal({ isOpen: false }); setActiveCarnetId(newCarnet.id); }; const handleDeleteCarnet = (id: string) => { if (window.confirm('Déplacer ce carnet et ses sous-carnets vers la corbeille ?')) { const idsToDelete = new Set([id]); const addChildren = (parentId: string) => { carnets.forEach(c => { if (c.parentId === parentId) { idsToDelete.add(c.id); addChildren(c.id); } }); }; addChildren(id); const deletedAt = new Date().toISOString(); setCarnets(carnets.map(c => idsToDelete.has(c.id) ? { ...c, isDeleted: true, deletedAt } : c)); setNotes(notes.map(n => idsToDelete.has(n.carnetId) ? { ...n, isDeleted: true, deletedAt } : n)); if (idsToDelete.has(activeCarnetId)) { setActiveCarnetId('1'); } } }; const handleDeleteNote = (id: string) => { const deletedAt = new Date().toISOString(); setNotes(notes.map(n => n.id === id ? { ...n, isDeleted: true, deletedAt } : n)); if (activeNoteId === id) setActiveNoteId(null); }; const handleRestoreCarnet = (id: string) => { setCarnets(carnets.map(c => c.id === id ? { ...c, isDeleted: false, deletedAt: undefined } : c)); // Optionally restore linked notes too? User might expect that. setNotes(notes.map(n => n.carnetId === id ? { ...n, isDeleted: false, deletedAt: undefined } : n)); }; const handleRestoreNote = (id: string) => { setNotes(notes.map(n => n.id === id ? { ...n, isDeleted: false, deletedAt: undefined } : n)); }; const handlePermanentDeleteNote = (id: string) => { setNotes(notes.filter(n => n.id !== id)); }; const handlePermanentDeleteCarnet = (id: string) => { const idsToDelete = new Set([id]); const addChildren = (parentId: string) => { carnets.forEach(c => { if (c.parentId === parentId) { idsToDelete.add(c.id); addChildren(c.id); } }); }; addChildren(id); setCarnets(carnets.filter(c => !idsToDelete.has(c.id))); setNotes(notes.filter(n => !idsToDelete.has(n.carnetId))); }; const handleAddNote = (e: React.FormEvent) => { e.preventDefault(); if (!newNoteTitle.trim() || !newNoteContent.trim()) return; const newNote: Note = { id: `n-${Date.now()}`, carnetId: activeCarnetId, title: newNoteTitle, date: new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric' }).format(new Date()), content: newNoteContent, imageUrl: 'https://images.unsplash.com/photo-1487958449943-2429e8be8625?auto=format&fit=crop&q=80&w=800&h=600', tags: [] }; setNotes([newNote, ...notes]); setNewNoteTitle(''); setNewNoteContent(''); setShowNewNoteModal(false); setActiveNoteId(newNote.id); }; const handleConvertIdeaToNote = (idea: BrainstormIdea) => { const newNote: Note = { id: `n-gen-${Date.now()}`, carnetId: activeCarnetId, title: idea.title, date: new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric' }).format(new Date()), content: `${idea.description}\n\n---\n**Connection to seed:** ${idea.connectionToSeed}\n**Novelty Score:** ${idea.noveltyScore}/10`, imageUrl: 'https://images.unsplash.com/photo-1497366216548-37526070297c?auto=format&fit=crop&q=80&w=800&h=600', tags: [{ id: 't-ai', label: 'AI Generated', type: 'ai' }] }; setNotes([newNote, ...notes]); setActiveView('notebooks'); setActiveNoteId(newNote.id); }; const handleUpdateNote = (updatedNote: Note) => { setNotes(prevNotes => { const existing = prevNotes.find(n => n.id === updatedNote.id); if (existing && updatedNote.isVersioningEnabled !== false) { const hasContentChanged = existing.content !== updatedNote.content; const hasTitleChanged = existing.title !== updatedNote.title; if (hasContentChanged || hasTitleChanged) { const history = existing.versionHistory || []; const lastSnapshot = history[0]; const isIdentical = lastSnapshot && lastSnapshot.content === existing.content && lastSnapshot.title === existing.title; if (!isIdentical) { const newSnapshot = { id: 'v-' + Date.now(), title: existing.title, content: existing.content, timestamp: new Date().toLocaleDateString('fr-FR', { day: 'numeric', month: 'short', year: 'numeric' }) + ' • ' + new Date().toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }), size: existing.content.length }; const updatedWithHistory = { ...updatedNote, versionHistory: [newSnapshot, ...history] }; return prevNotes.map(n => n.id === updatedNote.id ? updatedWithHistory : n); } } } return prevNotes.map(n => n.id === updatedNote.id ? updatedNote : n); }); }; // WebSocket Integration for Living Blocks const [wsConnected, setWsConnected] = React.useState(true); const [simulatedOffline, setSimulatedOffline] = React.useState(false); const socketRef = React.useRef(null); const initWebSocket = () => { if (simulatedOffline) return; try { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const socket = new WebSocket(`${protocol}//${window.location.host}`); socketRef.current = socket; socket.onopen = () => { setWsConnected(true); socket.send(JSON.stringify({ type: 'join', sessionId: 'global-momento-session', user: { id: 'u-1', name: 'Utilisateur Actuel' } })); }; socket.onmessage = (event) => { try { const data = JSON.parse(event.data); if (data.type === 'living_block_update') { const { sourceNoteId, blockIndex, newText } = data; setNotes(prevNotes => prevNotes.map(note => { if (note.id === sourceNoteId) { const paragraphs = note.content.split('\n'); if (paragraphs[blockIndex] !== undefined) { paragraphs[blockIndex] = newText; return { ...note, content: paragraphs.join('\n') }; } } return note; }) ); window.dispatchEvent(new CustomEvent('living-block-pulse', { detail: { sourceNoteId, blockIndex } })); } } catch (err) { console.error("Error parsing message", err); } }; socket.onclose = () => { setWsConnected(false); setTimeout(() => { if (socketRef.current === socket && !simulatedOffline) { initWebSocket(); } }, 4000); }; socket.onerror = () => { setWsConnected(false); }; } catch (e) { console.error(e); setWsConnected(false); } }; React.useEffect(() => { initWebSocket(); return () => { if (socketRef.current) { socketRef.current.close(); } }; }, [simulatedOffline]); React.useEffect(() => { const handleToggleSimulate = () => { setSimulatedOffline(prev => { const next = !prev; if (next) { if (socketRef.current) { socketRef.current.close(); } setWsConnected(false); } else { // Reconnect will automatically trigger because simulatedOffline changes } return next; }); }; window.addEventListener('toggle-websocket-simulate', handleToggleSimulate); return () => { window.removeEventListener('toggle-websocket-simulate', handleToggleSimulate); }; }, []); const broadcastLivingBlockUpdate = (sourceNoteId: string, blockIndex: number, newText: string) => { if (socketRef.current?.readyState === WebSocket.OPEN && !simulatedOffline) { socketRef.current.send(JSON.stringify({ type: 'living_block_update', sourceNoteId, blockIndex, newText })); } }; const handleEnterApp = () => { if (isAuthenticated) { setShowLanding(false); sessionStorage.setItem('momento-entered', 'true'); } else { setAuthMode('register'); setShowAuth(true); } }; const handleAuthComplete = () => { setIsAuthenticated(true); setShowAuth(false); setShowLanding(false); sessionStorage.setItem('momento-entered', 'true'); }; const handleLogout = () => { setIsAuthenticated(false); setShowLanding(true); sessionStorage.removeItem('momento-entered'); }; return ( {showLanding && !showAuth ? ( { setAuthMode('login'); setShowAuth(true); }} onRegister={() => { setAuthMode('register'); setShowAuth(true); }} /> ) : showAuth ? ( setShowAuth(false)} /> ) : ( setShowLanding(true)} onLogout={handleLogout} carnets={carnets} notes={notes} activeCarnetId={activeCarnetId} activeNoteId={activeNoteId} setActiveCarnetId={setActiveCarnetId} setActiveNoteId={setActiveNoteId} flashcards={flashcards} onSelectReviewDeck={(noteId) => { setActiveReviewDeckId(noteId); setActiveView('revision'); }} setShowNewCarnetModal={(show, parentId, isRenaming, carnetId) => { setShowNewCarnetModal({ isOpen: show, parentId, isRenaming, carnetId }); if (isRenaming && carnetId) { const carnet = carnets.find(c => c.id === carnetId); if (carnet) setNewCarnetName(carnet.name); } else { setNewCarnetName(''); } }} onDeleteCarnet={handleDeleteCarnet} onMoveCarnet={(draggedId, targetId) => { if (draggedId === targetId) return; // Basic circular check const isDescendant = (parentId: string, potentialChildId: string): boolean => { const childIds = carnets.filter(c => c.parentId === parentId).map(c => c.id); if (childIds.includes(potentialChildId)) return true; return childIds.some(id => isDescendant(id, potentialChildId)); }; if (targetId && isDescendant(draggedId, targetId)) { console.warn("Cannot move a notebook inside its own descendant"); return; } setCarnets(prev => prev.map(c => c.id === draggedId ? { ...c, parentId: targetId } : c)); }} />
{(activeView === 'notebooks' || activeView === 'shared' || activeView === 'reminders') && ( setShowNewCarnetModal({ isOpen: show, parentId, isRenaming, carnetId })} onDeleteNote={handleDeleteNote} onBrainstormNote={handleBrainstormNote} onUpdateNote={handleUpdateNote} onOpenSidebar={() => setIsSidebarOpen(true)} onSearchClick={() => setIsSearchOpen(true)} wsConnected={wsConnected} broadcastLivingBlockUpdate={broadcastLivingBlockUpdate} carnets={carnets} flashcards={flashcards} onTriggerReviewDeck={(noteId) => { setActiveReviewDeckId(noteId); setActiveView('revision'); }} onGenerateFlashcards={handleGenerateFlashcards} isGeneratingFlashcards={isGeneratingFlashcards} /> )} {activeView === 'trash' && ( n.isDeleted)} deletedCarnets={carnets.filter(c => c.isDeleted)} onRestoreNote={handleRestoreNote} onRestoreCarnet={handleRestoreCarnet} onPermanentDeleteNote={handlePermanentDeleteNote} onPermanentDeleteCarnet={handlePermanentDeleteCarnet} onEmptyTrash={() => { setNotes(notes.filter(n => !n.isDeleted)); setCarnets(carnets.filter(c => !c.isDeleted)); }} onOpenSidebar={() => setIsSidebarOpen(true)} /> )} {activeView === 'agents' && ( setNotes([note, ...notes])} onOpenSidebar={() => setIsSidebarOpen(true)} /> )} {activeView === 'settings' && ( setIsSidebarOpen(true)} /> )} {activeView === 'brainstorm' && ( )} {activeView === 'insights' && ( { setActiveView('notebooks'); setActiveNoteId(noteId); }} onOpenSidebar={() => setIsSidebarOpen(true)} /> )} {activeView === 'temporal' && ( { setActiveView('notebooks'); setActiveNoteId(noteId); }} /> )} {activeView === 'graph' && ( { setActiveView('notebooks'); setActiveNoteId(noteId); const note = notes.find(n => n.id === noteId); if (note) { setActiveCarnetId(note.carnetId); } }} onClose={() => setActiveView('notebooks')} /> )} {activeView === 'revision' && ( { setFlashcards(updated); localStorage.setItem('momento-flashcards', JSON.stringify(updated)); }} onSelectNote={(noteId) => { setActiveView('notebooks'); setActiveNoteId(noteId); const note = notes.find(n => n.id === noteId); if (note) { setActiveCarnetId(note.carnetId); } }} onOpenSidebar={() => setIsSidebarOpen(true)} initialActiveDeckId={activeReviewDeckId} onClearActiveDeckId={() => setActiveReviewDeckId(null)} /> )}
{/* Modals */} {showNewCarnetModal.isOpen && (
setShowNewCarnetModal({ isOpen: false })} className="absolute inset-0 bg-ink/40 backdrop-blur-sm" />

{showNewCarnetModal.isRenaming ? 'Rename Carnet' : (showNewCarnetModal.parentId ? 'Create Sub-Carnet' : 'Create New Carnet')}

{showNewCarnetModal.parentId && !showNewCarnetModal.isRenaming && (

Inside: {carnets.find(c => c.id === showNewCarnetModal.parentId)?.name}

)}
setNewCarnetName(e.target.value)} placeholder="E.g., Sustainable Patterns" className="w-full bg-white dark:bg-[#2A2A2A] border border-border rounded-lg px-4 py-3 outline-none focus:border-ink transition-colors font-serif italic text-lg text-ink dark:text-dark-ink" />
)} {showNewNoteModal && (
setShowNewNoteModal(false)} className="absolute inset-0 bg-ink/40 backdrop-blur-sm" /> {slashMenu?.isOpen && ( { console.log(type); setSlashMenu(null); }} onClose={() => setSlashMenu(null)} /> )}

Add Architectural Note

setNewNoteTitle(e.target.value)} placeholder="Enter the title of your study..." className="w-full bg-white dark:bg-[#2A2A2A] border border-border rounded-lg px-5 py-4 outline-none focus:border-ink transition-colors font-serif text-2xl text-ink dark:text-dark-ink" />