import React from 'react'; import { Plus, Search, Share2, Pin, ChevronRight, ChevronUp, ChevronDown, ArrowLeft, MoreVertical, Sparkles, Tag as TagIcon, X, BookOpen, Edit3, Eye, Trash2, Wind, FileText, Paperclip, Loader2, MessageSquare, Menu, Globe, Link2, Folder, LayoutGrid, List, Table, CheckSquare, GraduationCap, PanelRight } from 'lucide-react'; import { motion, AnimatePresence } from 'motion/react'; import { Note, Carnet, Tag, Attachment, Flashcard } from '../types'; import { SlashMenu } from './SlashMenu'; import { parseDocument } from '../services/geminiService'; import { v4 as uuidv4 } from 'uuid'; import { LivingBlock } from './LivingBlock'; import { BlockPicker } from './BlockPicker'; import { NotebookInfoSidebar } from './NotebookInfoSidebar'; import { ModernBlockNoteEditor } from './ModernBlockNoteEditor'; interface NotebooksViewProps { activeNoteId: string | null; activeCarnet: Carnet | undefined; filteredNotes: Note[]; activeNote: Note | undefined; setActiveNoteId: (id: string | null) => void; togglePin: (id: string) => void; setShowNewNoteModal: (show: boolean) => void; isAISidebarOpen: boolean; setIsAISidebarOpen: (open: boolean) => void; selectedTagIds: string[]; setSelectedTagIds: (ids: string[]) => void; allNotes: Note[]; activeCarnetId: string; setShowNewCarnetModal: (show: boolean, parentId?: string, isRenaming?: boolean, carnetId?: string) => void; onDeleteNote: (id: string) => void; onBrainstormNote: (note: Note) => void; onUpdateNote?: (note: Note) => void; onOpenSidebar?: () => void; onSearchClick?: () => void; wsConnected?: boolean; broadcastLivingBlockUpdate?: (sourceNoteId: string, blockIndex: number, newText: string) => void; carnets?: Carnet[]; // Optional carnets list flashcards?: Flashcard[]; onTriggerReviewDeck?: (noteId: string) => void; onGenerateFlashcards?: (noteId: string) => Promise; isGeneratingFlashcards?: boolean; } export const NotebooksView: React.FC = ({ activeNoteId, activeCarnet, filteredNotes, activeNote, setActiveNoteId, togglePin, setShowNewNoteModal, isAISidebarOpen, setIsAISidebarOpen, selectedTagIds, setSelectedTagIds, allNotes, activeCarnetId, setShowNewCarnetModal, onDeleteNote, onBrainstormNote, onUpdateNote, onOpenSidebar, onSearchClick, wsConnected = true, broadcastLivingBlockUpdate, carnets = [], flashcards = [], onTriggerReviewDeck, onGenerateFlashcards, isGeneratingFlashcards = false }) => { const [isTagsExpanded, setIsTagsExpanded] = React.useState(false); const [tagSearchQuery, setTagSearchQuery] = React.useState(''); const [isEditing, setIsEditing] = React.useState(false); const [isNoteInfoOpen, setIsNoteInfoOpen] = React.useState(false); const [slashMenu, setSlashMenu] = React.useState<{ isOpen: boolean; top: number; left: number } | null>(null); const [isAnalyzing, setIsAnalyzing] = React.useState(null); const [activeDocQnA, setActiveDocQnA] = React.useState(null); const [isPickerOpen, setIsPickerOpen] = React.useState(false); const [prefilledBlock, setPrefilledBlock] = React.useState<{ noteId: string; blockIndex: number } | null>(null); // Flashcards state computations const noteFlashcards = React.useMemo(() => { if (!activeNote || !flashcards) return []; return flashcards.filter(c => c.noteId === activeNote.id); }, [flashcards, activeNote]); const nextRecallText = React.useMemo(() => { if (noteFlashcards.length === 0) return ''; let minDate = noteFlashcards[0].nextReviewDate; noteFlashcards.forEach(c => { if (c.nextReviewDate < minDate) { minDate = c.nextReviewDate; } }); const diff = new Date(minDate).getTime() - Date.now(); if (diff <= 0) return "Dû aujourd'hui"; const days = Math.ceil(diff / (24 * 60 * 60 * 1000)); return `dans ${days}j`; }, [noteFlashcards]); // Vue Structurée states const [viewType, setViewType] = React.useState<'notes' | 'tasks'>('notes'); const [layoutMode, setLayoutMode] = React.useState<'grid' | 'list' | 'table'>('list'); const [sortColumn, setSortColumn] = React.useState<'title' | 'carnet' | 'labels' | 'tasks' | 'modified' | null>(null); const [sortDirection, setSortDirection] = React.useState<'asc' | 'desc' | null>(null); // Helper mapping french relative dates for architecture aesthetics const getRelativeFrenchDate = (dateStr: string): string => { if (dateStr.includes('Oct 26')) return 'il y a 2h'; if (dateStr.includes('Oct 27')) return 'il y a 1j'; if (dateStr.includes('Oct 24')) return 'il y a 3j'; if (dateStr.includes('Oct 25')) return 'il y a 2j'; if (dateStr.includes('Oct 22')) return 'il y a 5j'; if (dateStr.includes('Oct 23')) return 'il y a 4j'; if (dateStr.includes('Oct 28')) return 'il y a 10 min'; // Fallback calculation relative to May 24, 2026 try { const d = new Date(dateStr); const now = new Date("2026-05-24T07:18:49Z"); const diffTime = Math.abs(now.getTime() - d.getTime()); const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); if (diffDays <= 1) return "aujourd'hui"; if (diffDays <= 2) return "hier"; if (diffDays <= 7) return `il y a ${diffDays}j`; if (diffDays <= 30) return `il y a ${Math.floor(diffDays / 7)} sem.`; return `il y a ${Math.floor(diffDays / 30)} mois`; } catch (e) { return dateStr; } }; // Aesthetic deterministic carnet colors const getCarnetColor = (c: Carnet) => { const colors = [ { bg: 'bg-[#A47148]/5 dark:bg-[#A47148]/10', border: 'border-[#A47148]/20', text: 'text-[#A47148]' }, { bg: 'bg-emerald-500/5 dark:bg-emerald-500/10', border: 'border-emerald-500/15', text: 'text-emerald-600 dark:text-emerald-400' }, { bg: 'bg-indigo-500/5 dark:bg-indigo-500/10', border: 'border-indigo-500/15', text: 'text-indigo-600 dark:text-indigo-400' }, { bg: 'bg-blue-500/5 dark:bg-blue-500/10', border: 'border-blue-500/15', text: 'text-blue-600 dark:text-blue-400' }, { bg: 'bg-amber-500/5 dark:bg-amber-500/10', border: 'border-amber-500/15', text: 'text-amber-600 dark:text-amber-400' }, { bg: 'bg-rose-500/5 dark:bg-rose-500/10', border: 'border-rose-500/15', text: 'text-rose-600 dark:text-rose-400' }, ]; if (c.type === 'Private') return colors[0]; if (c.type === 'Shared') return colors[2]; const idx = Math.abs(c.name.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % colors.length; return colors[idx]; }; // Extract tasks from markdown format: - [ ] Task name const getNoteTasksStats = (content: string) => { const lines = content.split('\n'); let total = 0; let completed = 0; lines.forEach(line => { const match = line.match(/^\s*[-*]?\s*\[([ xX])\]\s*(.*)$/); if (match) { total++; if (match[1].toLowerCase() === 'x') { completed++; } } }); return { completed, total }; }; interface TaskItem { id: string; noteId: string; noteTitle: string; text: string; completed: boolean; lineIndex: number; } const extractTasks = React.useMemo(() => { const tasksList: TaskItem[] = []; filteredNotes.forEach(note => { const lines = note.content.split('\n'); lines.forEach((line, idx) => { const match = line.match(/^\s*[-*]?\s*\[([ xX])\]\s*(.*)$/); if (match) { tasksList.push({ id: `${note.id}-${idx}`, noteId: note.id, noteTitle: note.title, text: match[2].trim(), completed: match[1].toLowerCase() === 'x', lineIndex: idx }); } }); }); return tasksList; }, [filteredNotes]); const completedTasksCount = React.useMemo(() => { return extractTasks.filter(t => t.completed).length; }, [extractTasks]); const handleToggleTask = (task: TaskItem) => { const note = allNotes.find(n => n.id === task.noteId); if (!note) return; const lines = note.content.split('\n'); const line = lines[task.lineIndex]; if (line) { const isNowCompleted = !task.completed; const nextChar = isNowCompleted ? 'x' : ' '; const updatedLine = line.replace(/\[([ xX])\]/, `[${nextChar}]`); lines[task.lineIndex] = updatedLine; const updatedNote = { ...note, content: lines.join('\n') }; if (onUpdateNote) { onUpdateNote(updatedNote); } } }; // Click handler to toggle sorting order const handleSort = (field: 'title' | 'carnet' | 'labels' | 'tasks' | 'modified') => { if (sortColumn !== field) { setSortColumn(field); setSortDirection('asc'); } else if (sortDirection === 'asc') { setSortDirection('desc'); } else { setSortColumn(null); setSortDirection(null); } }; // Computes the sorted copy of filteredNotes const sortedNotes = React.useMemo(() => { if (!sortColumn || !sortDirection) return filteredNotes; const notesCopy = [...filteredNotes]; return notesCopy.sort((a, b) => { let valA: any = ''; let valB: any = ''; if (sortColumn === 'title') { valA = (a.title || '').toLowerCase(); valB = (b.title || '').toLowerCase(); } else if (sortColumn === 'carnet') { const parentA = carnets?.find(c => c.id === a.carnetId)?.name || ''; const parentB = carnets?.find(c => c.id === b.carnetId)?.name || ''; valA = parentA.toLowerCase(); valB = parentB.toLowerCase(); } else if (sortColumn === 'labels') { valA = a.tags?.length || 0; valB = b.tags?.length || 0; } else if (sortColumn === 'tasks') { const statsA = getNoteTasksStats(a.content); const statsB = getNoteTasksStats(b.content); valA = statsA.total > 0 ? (statsA.completed / statsA.total) : -1; valB = statsB.total > 0 ? (statsB.completed / statsB.total) : -1; } else if (sortColumn === 'modified') { valA = new Date(a.date).getTime() || 0; valB = new Date(b.date).getTime() || 0; } if (valA < valB) return sortDirection === 'asc' ? -1 : 1; if (valA > valB) return sortDirection === 'asc' ? 1 : -1; return 0; }); }, [filteredNotes, sortColumn, sortDirection, carnets]); const renderSortIndicator = (field: 'title' | 'carnet' | 'labels' | 'tasks' | 'modified') => { if (sortColumn !== field) return null; return sortDirection === 'asc' ? ( ) : ( ); }; const fileInputRef = React.useRef(null); const titleInputRef = React.useRef(null); const contentTextareaRef = React.useRef(null); const backlinks = React.useMemo(() => { if (!activeNote) return []; const list: Array<{ note: Note; accessedAt: string }> = []; allNotes.forEach(note => { if (note.id === activeNote.id) return; if (note.content.includes(`[[living-block:${activeNote.id}:`)) { list.push({ note, accessedAt: "Récemment" }); } }); return list; }, [allNotes, activeNote]); const memoryEchoBlock = React.useMemo(() => { if (!activeNote) return null; const otherNotes = allNotes.filter(n => n.id !== activeNote.id); if (otherNotes.length === 0) return null; let bestNote = otherNotes[0]; let maxOverlap = -1; const currentTags = new Set(activeNote.tags?.map(t => t.id) || []); otherNotes.forEach(note => { const overlap = note.tags?.filter(t => currentTags.has(t.id)).length || 0; if (overlap > maxOverlap) { maxOverlap = overlap; bestNote = note; } }); const lines = bestNote.content.split('\n').filter(line => line.trim().length > 30 && !line.startsWith('#') && !line.startsWith('[[living-block')); if (lines.length === 0) return null; const blockText = lines[0]; const blockIndex = bestNote.content.split('\n').indexOf(blockText); return { noteId: bestNote.id, noteTitle: bestNote.title || "Note reliée", text: blockText, blockIndex }; }, [activeNote, allNotes]); const handleSelectBlock = (sourceNoteId: string, blockIndex: number) => { setIsPickerOpen(false); setPrefilledBlock(null); if (!activeNote) return; const blockCode = `\n[[living-block:${sourceNoteId}:${blockIndex}]]\n`; // Insert blockCode into textarea at cursor or end of text const textarea = contentTextareaRef.current; if (textarea) { const start = textarea.selectionStart; const end = textarea.selectionEnd; const originalText = textarea.value; const newText = originalText.substring(0, start) + blockCode + originalText.substring(end); const updatedNote = { ...activeNote, content: newText }; onUpdateNote?.(updatedNote); // Update defaultValue/value of textarea textarea.value = newText; } else { // Fallback: append to end of content const updatedNote = { ...activeNote, content: activeNote.content + blockCode }; onUpdateNote?.(updatedNote); } }; const handleFileUpload = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file || !activeNote) return; const newAttachment: Attachment = { id: uuidv4(), name: file.name, type: file.name.endsWith('.pdf') ? 'pdf' : (file.name.endsWith('.docx') ? 'docx' : 'other'), url: URL.createObjectURL(file), // Local preview url isProcessed: false }; const updatedNote = { ...activeNote, attachments: [...(activeNote.attachments || []), newAttachment] }; onUpdateNote?.(updatedNote); // Auto-analyze setIsAnalyzing(newAttachment.id); const content = await parseDocument(newAttachment.url, newAttachment.name); const processedAttachment = { ...newAttachment, content, isProcessed: true }; const finalNote = { ...activeNote, attachments: [...(activeNote.attachments || []), processedAttachment] }; onUpdateNote?.(finalNote); setIsAnalyzing(null); }; 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 }); } } }; const insertCommand = (type: string) => { console.log(`Command selected: ${type}`); setSlashMenu(null); if (type === 'embed') { setPrefilledBlock(null); setIsPickerOpen(true); } }; const availableTags = React.useMemo(() => { const carnetNotes = allNotes.filter(n => n.carnetId === activeCarnetId); const tagsMap = new Map(); carnetNotes.forEach(note => { note.tags?.forEach(tag => { tagsMap.set(tag.id, tag); }); }); return Array.from(tagsMap.values()).sort((a, b) => { // AI tags first, then alphabetical if (a.type === 'ai' && b.type !== 'ai') return -1; if (a.type !== 'ai' && b.type === 'ai') return 1; return a.label.localeCompare(b.label); }); }, [allNotes, activeCarnetId]); const visibleTags = React.useMemo(() => { let filtered = availableTags; if (tagSearchQuery) { filtered = availableTags.filter(t => t.label.toLowerCase().includes(tagSearchQuery.toLowerCase()) ); } else if (!isTagsExpanded) { filtered = availableTags.slice(0, 10); // Ensure selected tags are always visible even if not in the first 10 selectedTagIds.forEach(id => { if (!filtered.find(t => t.id === id)) { const tag = availableTags.find(t => t.id === id); if (tag) filtered.push(tag); } }); } return filtered; }, [availableTags, isTagsExpanded, tagSearchQuery, selectedTagIds]); const toggleTag = (tagId: string) => { if (selectedTagIds.includes(tagId)) { setSelectedTagIds(selectedTagIds.filter(id => id !== tagId)); } else { setSelectedTagIds([...selectedTagIds, tagId]); } }; if (!activeNoteId) { return (

{activeCarnet?.name} — {filteredNotes[0]?.date || 'Oct 26'}

{/* Onglet Notes / Tâches */}
{/* Toggle 3 états pour Notes */} {viewType === 'notes' && (
{sortedNotes.length === 0 && (

This notebook is waiting for its first vision.

)}
) : ( // ================== ORIGINAL DETAILED EDITORIAL LIST VIEW ==================
{filteredNotes.map((note, index) => ( setActiveNoteId(note.id)} >

{note.isPinned && } {note.title}

{note.title}
{note.tags?.map(tag => (
{tag.type === 'ai' && } {tag.label}
))}

{note.content}

Read more
))} {filteredNotes.length === 0 && (

This notebook is waiting for its first vision.

)}
)}

© 2024 Architectural Grid. All rights reserved.

); } return (
{/* Native simulated connection status indicator toggle */}
{slashMenu?.isOpen && ( insertCommand(type)} onClose={() => setSlashMenu(null)} /> )} {activeNote?.isClipped && (
{activeNote.clipFavicon ? ( favicon { (e.target as any).src = 'https://www.google.com/s2/favicons?domain=google.com'; }} /> ) : ( )} {activeNote.clipSourceUrl}
Source web Capturé : {activeNote.clipDate || activeNote.date}
)}
{activeCarnet?.name} {activeNote?.date} {activeNote?.isClipped && ( <> Source web )}
{isEditing ? ( ) : (

{activeNote?.title}

)}
{activeNote?.tags?.map(tag => (
{tag.type === 'ai' && } {tag.label} {tag.type === 'ai' && (
)}
))}
{/* Flashcards alert state indicator (E) */} {activeNote && noteFlashcards.length > 0 && (
{noteFlashcards.length} flashcards générées · Prochain rappel : {nextRecallText}
)}
{activeNote?.title}
{activeNote?.attachments && activeNote.attachments.length > 0 && (

Pièces jointes ({activeNote.attachments.length})

{activeNote.attachments.map(att => (

{att.name}

{att.type}

{isAnalyzing === att.id ? ( ) : ( )}
))}
)} {activeNote && ( {})} allNotes={allNotes} /> )}
{/* Memory Echo Section */} {memoryEchoBlock && (
Memory Echo (Affinité Sémantique)
92% affinité sémétrique

« {memoryEchoBlock.text.substring(0, 150)}... »

Passage détecté dans : {memoryEchoBlock.noteTitle}
)} {/* Backlinks panel */} {backlinks.length > 0 && (
Rétroliens & Intégrations Sémantiques
Embeddé comme Living Block dans {backlinks.length} note{backlinks.length > 1 ? 's' : ''} :
{backlinks.map(({ note, accessedAt }) => (
setActiveNoteId(note.id)} className="p-3.5 bg-white dark:bg-zinc-800/20 border border-black/[0.04] dark:border-white/[0.04] rounded-xl hover:bg-black/[0.02] dark:hover:bg-white/[0.02] hover:border-accent/20 transition-all cursor-pointer flex items-center justify-between pr-4 group" >
{note.title || "Note sans titre"} Carnet : {carnets?.find(c => c.id === note.carnetId)?.name || "Général"}
Accès : {accessedAt}
))}
)}
{/* Document Q&A Overlay */} {activeDocQnA && (
{/* Document Preview (Mock) */}

{activeDocQnA.name}

DOCUMENT SOURCE ANALYSÉ

{activeDocQnA.content || "Analyse du contenu en cours..."}
{/* Chat Side */}

Expert Document

Bonjour ! J'ai analysé ce document. Posez-moi n'importe quelle question sur son contenu, les chiffres clés ou les concepts abordés.