Files
Momento/architectural-grid1/src/components/NotebooksView.tsx
Antigravity f46654f574 feat: editor improvements and architectural grid prototype
Multiple feature additions and improvements across the application:

- NextGen Editor: drag handles, smart paste, block actions
- Structured views: Kanban and table layouts for notes
- Architectural Grid: new brainstorming/agent interface prototype
- Flashcards: SM-2 revision algorithm with AI generation
- MCP server: robustness improvements
- Graph/PDF chat: fix click propagation and copy behavior
- Various UI/UX enhancements and bug fixes

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 19:45:15 +00:00

1533 lines
72 KiB
TypeScript

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<void>;
isGeneratingFlashcards?: boolean;
}
export const NotebooksView: React.FC<NotebooksViewProps> = ({
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<string | null>(null);
const [activeDocQnA, setActiveDocQnA] = React.useState<Attachment | null>(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' ? (
<ChevronUp size={10} className="text-accent ml-1 inline-block transition-transform duration-200" />
) : (
<ChevronDown size={10} className="text-accent ml-1 inline-block transition-transform duration-200" />
);
};
const fileInputRef = React.useRef<HTMLInputElement>(null);
const titleInputRef = React.useRef<HTMLInputElement>(null);
const contentTextareaRef = React.useRef<HTMLTextAreaElement>(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<HTMLInputElement>) => {
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<string, Tag>();
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 (
<div className="h-full flex flex-col overflow-y-auto">
<header className="px-6 sm:px-12 pt-12 pb-8 flex flex-col gap-6 sticky top-0 bg-paper/80 backdrop-blur-md z-30">
<div className="flex justify-between items-start gap-4">
<button
onClick={onOpenSidebar}
className="lg:hidden p-2 -ml-2 text-ink hover:bg-black/5 rounded-lg transition-colors"
>
<Menu size={20} />
</button>
<h1 className="text-3xl sm:text-4xl font-serif font-medium tracking-tight text-ink leading-tight flex-1 pr-6">
{activeCarnet?.name} {filteredNotes[0]?.date || 'Oct 26'}
</h1>
</div>
<div className="flex items-center justify-between border-b border-ink/5 pb-4 flex-wrap gap-4 select-none">
<div className="flex items-center gap-6">
<button
onClick={() => setShowNewNoteModal(true)}
className="flex items-center gap-2 text-[13px] text-ink font-medium hover:opacity-70 transition-opacity cursor-pointer"
>
<Plus size={16} />
<span>Add Note</span>
</button>
<button
onClick={() => setShowNewCarnetModal(true, activeCarnetId)}
className="flex items-center gap-2 text-[13px] text-concrete font-medium hover:text-ink transition-all cursor-pointer"
>
<BookOpen size={16} />
<span>New Sub-Carnet</span>
</button>
<button
onClick={onSearchClick}
className="flex items-center gap-2 text-[13px] text-ink font-medium hover:opacity-70 transition-opacity cursor-pointer"
>
<Search size={16} />
<span>Search</span>
</button>
</div>
<div className="flex items-center gap-4 flex-wrap">
{/* Onglet Notes / Tâches */}
<div className="bg-black/[0.03] dark:bg-white/[0.04] p-0.5 rounded-full flex border border-border/30">
<button
onClick={() => setViewType('notes')}
className={`px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-wider transition-all cursor-pointer
${viewType === 'notes'
? 'bg-ink text-paper dark:bg-white dark:text-black shadow-sm'
: 'text-concrete hover:text-ink'}`}
>
Notes
</button>
<button
onClick={() => setViewType('tasks')}
className={`px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-wider transition-all cursor-pointer
${viewType === 'tasks'
? 'bg-ink text-paper dark:bg-white dark:text-black shadow-sm'
: 'text-concrete hover:text-ink'}`}
>
Tâches
</button>
</div>
{/* Toggle 3 états pour Notes */}
{viewType === 'notes' && (
<div className="bg-black/[0.03] dark:bg-white/[0.04] p-0.5 rounded-full flex border border-border/30 items-center">
<button
onClick={() => setLayoutMode('grid')}
className={`p-1.5 rounded-full transition-all cursor-pointer ${layoutMode === 'grid' ? 'bg-ink text-paper dark:bg-white dark:text-black shadow-sm' : 'text-concrete hover:text-ink'}`}
title="Vue cartes / grille"
>
<LayoutGrid size={13} />
</button>
<button
onClick={() => setLayoutMode('list')}
className={`p-1.5 rounded-full transition-all cursor-pointer ${layoutMode === 'list' ? 'bg-ink text-paper dark:bg-white dark:text-black shadow-sm' : 'text-concrete hover:text-ink'}`}
title="Vue liste éditoriale"
>
<List size={13} />
</button>
<button
onClick={() => setLayoutMode('table')}
className={`p-1.5 rounded-full transition-all cursor-pointer ${layoutMode === 'table' ? 'bg-ink text-paper dark:bg-white dark:text-black shadow-sm' : 'text-concrete hover:text-ink'}`}
title="Vue tableau structuré"
>
<Table size={13} />
</button>
</div>
)}
<button className="flex items-center gap-2 text-[13px] text-ink font-medium hover:opacity-70 transition-opacity cursor-pointer">
<Share2 size={16} />
<span>Share</span>
</button>
</div>
</div>
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3 text-[10px] font-bold uppercase tracking-[0.2em] text-concrete">
<TagIcon size={12} />
<span>Filter by Tags</span>
{selectedTagIds.length > 0 && (
<span className="bg-accent/10 text-accent px-2 py-0.5 rounded-full text-[9px] lowercase tracking-normal">
{selectedTagIds.length} active
</span>
)}
</div>
{availableTags.length > 10 && (
<div className="relative group">
<input
type="text"
placeholder="Search tags..."
className="bg-transparent border-b border-border/40 text-[10px] outline-none focus:border-accent/40 py-1 px-2 w-32 transition-all focus:w-48 placeholder:text-concrete/40"
onChange={(e) => setTagSearchQuery(e.target.value)}
/>
</div>
)}
</div>
<div className="flex flex-wrap gap-2 items-center min-h-[32px]">
<AnimatePresence mode="popLayout">
{visibleTags.map(tag => {
const isActive = selectedTagIds.includes(tag.id);
return (
<motion.button
layout
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
key={tag.id}
onClick={() => toggleTag(tag.id)}
className={`px-3 py-1.5 rounded-full text-[10px] font-bold uppercase tracking-wider transition-all border flex items-center gap-2
${isActive
? 'bg-ink text-paper border-ink shadow-lg shadow-ink/10'
: 'bg-white/40 border-border text-concrete hover:border-concrete/40 hover:bg-white/60'}`}
>
{tag.type === 'ai' && (
<Sparkles
size={10}
className={isActive ? 'text-accent' : 'text-accent/60'}
/>
)}
{tag.label}
{isActive && <X size={10} />}
</motion.button>
);
})}
</AnimatePresence>
{availableTags.length > 10 && !tagSearchQuery && (
<button
onClick={() => setIsTagsExpanded(!isTagsExpanded)}
className="px-3 py-1.5 text-[10px] font-bold uppercase tracking-wider text-concrete/60 hover:text-ink transition-colors border border-dashed border-border rounded-full"
>
{isTagsExpanded ? 'Show less' : `+ ${availableTags.length - 10} more`}
</button>
)}
{selectedTagIds.length > 0 && (
<button
onClick={() => setSelectedTagIds([])}
className="px-3 py-1.5 text-[10px] font-bold uppercase tracking-wider text-rust hover:underline ml-auto"
>
Clear all
</button>
)}
</div>
</div>
</header>
<div className="px-6 md:px-12 flex-1 pb-20 pt-6">
{viewType === 'tasks' ? (
// ================== FLAT TASKS LIST VIEW ==================
<div className="max-w-4xl mx-auto space-y-6">
<div className="flex items-center justify-between pb-3 border-b border-black/[0.05] dark:border-white/[0.05]">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
<span className="text-[10px] uppercase font-bold tracking-[0.2em] text-concrete">
Ravi de vous revoir Tâches d'Architecture
</span>
</div>
<div className="text-[11px] font-mono font-bold text-ink bg-black/[0.03] dark:bg-white/5 py-1 px-3 rounded-full">
{extractTasks.length} tâches · {completedTasksCount} complétées
</div>
</div>
{extractTasks.length > 0 ? (
<div className="overflow-hidden border border-[#D5D2CD]/40 dark:border-neutral-800/40 rounded-2xl bg-paper dark:bg-[#1C1C1E]/20 shadow-sm">
<div className="divide-y divide-black/[0.04] dark:divide-white/[0.04]">
{extractTasks.map((task, idx) => (
<div
key={task.id}
className="p-4 flex items-center justify-between gap-4 hover:bg-black/[0.01] dark:hover:bg-white/[0.01] transition-all group"
>
<div className="flex items-center gap-3.5 flex-grow min-w-0">
<button
onClick={() => handleToggleTask(task)}
className={`w-5 h-5 rounded-md border flex items-center justify-center transition-all cursor-pointer shrink-0
${task.completed
? 'bg-[#A47148] border-[#A47148] text-white'
: 'border-[#D5D2CD] dark:border-neutral-700 hover:border-accent bg-transparent'}`}
>
{task.completed && <span className="text-xs font-bold font-sans">✓</span>}
</button>
<span className={`text-[13px] font-light leading-relaxed truncate text-ink/80
${task.completed ? 'line-through text-concrete decoration-concrete' : 'dark:text-white/95'}`}
>
{task.text}
</span>
</div>
<div className="flex items-center gap-2 shrink-0">
<span className="text-[9.5px] uppercase font-mono tracking-wider text-concrete dark:text-neutral-500 max-w-[140px] truncate">
Note : {task.noteTitle}
</span>
<button
onClick={() => setActiveNoteId(task.noteId)}
className="p-1.5 rounded-full hover:bg-black/5 dark:hover:bg-white/5 text-concrete hover:text-ink transition-all cursor-pointer"
title="Ouvrir la note source"
>
<Link2 size={13} />
</button>
</div>
</div>
))}
</div>
</div>
) : (
// ================== EMPTY STATE TASKS ==================
<div className="h-64 flex flex-col items-center justify-center text-center space-y-4">
<div className="w-12 h-12 rounded-full bg-neutral-100 dark:bg-zinc-800/50 flex items-center justify-center border border-border/40 text-concrete animate-pulse">
<CheckSquare size={18} className="text-concrete/60" />
</div>
<p className="font-serif text-lg italic text-muted-ink">Aucune tâche dans ce carnet</p>
<p className="text-xs text-concrete/70 max-w-sm leading-relaxed font-light">
Ajoutez des lignes de tâches au format <code className="font-mono text-[10.5px] bg-black/[0.04] dark:bg-white/5 p-1 px-1.5 rounded text-ink dark:text-amber-200">- [ ] Ma tâche</code> dans vos notes pour commencer.
</p>
</div>
)}
</div>
) : layoutMode === 'grid' ? (
// ================== CARDS/GRID VIEW ==================
<div className="max-w-6xl mx-auto space-y-8">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{sortedNotes.map((note, index) => {
const stats = getNoteTasksStats(note.content);
return (
<motion.div
initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.04 * index, duration: 0.5 }}
key={note.id}
onClick={() => setActiveNoteId(note.id)}
className="bg-white dark:bg-[#1C1C1E]/40 border border-[#D5D2CD]/40 dark:border-neutral-800/40 rounded-2xl overflow-hidden hover:shadow-md hover:border-accent/30 transition-all duration-300 group/card cursor-pointer flex flex-col"
>
{/* Thumbnail with black white architectural look */}
<div className="aspect-[16/10] bg-neutral-100 dark:bg-neutral-900 border-b border-[#D5D2CD]/20 dark:border-neutral-800/20 overflow-hidden relative">
<img
src={note.imageUrl}
alt={note.title}
className="w-full h-full object-cover mix-blend-multiply dark:mix-blend-normal opacity-85 grayscale contrast-115 group-hover/card:scale-105 group-hover/card:grayscale-0 group-hover/card:opacity-100 transition-all duration-500"
referrerPolicy="no-referrer"
/>
{note.isPinned && (
<div className="absolute top-3 left-3 bg-paper/90 dark:bg-dark-paper/90 backdrop-blur-sm p-1.5 rounded-full shadow-sm border border-border/40 text-amber-500">
<Pin size={11} className="fill-amber-500" />
</div>
)}
{stats.total > 0 && (
<div className="absolute top-3 right-3 bg-paper/90 dark:bg-dark-paper/90 backdrop-blur-sm py-1 px-2.5 rounded-full shadow-sm border border-border/40 text-[9.5px] font-mono font-bold text-concrete">
{stats.completed}/{stats.total} ✓
</div>
)}
</div>
<div className="p-5 flex-1 flex flex-col justify-between space-y-4">
<div className="space-y-2.5">
<div className="flex flex-wrap gap-1.5">
{note.tags?.slice(0, 2).map(tag => (
<span
key={tag.id}
className={`px-1.5 py-0.5 rounded text-[8px] font-bold uppercase tracking-wider border flex items-center gap-1
${tag.type === 'ai'
? 'bg-accent/5 border-accent/15 text-accent'
: 'bg-concrete/5 border-border text-concrete'}`}
>
{tag.type === 'ai' && <Sparkles size={6} />}
{tag.label}
</span>
))}
</div>
<h3 className="font-serif text-base font-semibold text-ink leading-snug group-hover/card:text-accent transition-colors flex items-center justify-between gap-2">
<span className="truncate">{note.title}</span>
</h3>
<p className="text-xs text-concrete dark:text-neutral-400 leading-relaxed line-clamp-3 font-light text-justify">
{note.content.replace(/[-*]?\s*\[([ xX])\]\s*(.*)/g, '').trim().substring(0, 110)}...
</p>
</div>
<div className="flex items-center justify-between pt-3 border-t border-black/[0.03] dark:border-white/[0.03] text-[9.5px] text-concrete font-medium uppercase tracking-wider">
<span>{note.date}</span>
<div className="flex items-center gap-1 opacity-0 group-hover/card:opacity-100 transition-opacity">
<button
onClick={(e) => {
e.stopPropagation();
onBrainstormNote(note);
}}
className="p-1 px-2 rounded-full hover:bg-ochre/10 text-ochre transition-all flex items-center gap-1 cursor-pointer"
title="Brainstorm this concept"
>
<Wind size={11} />
</button>
<button
onClick={(e) => {
e.stopPropagation();
togglePin(note.id);
}}
className="p-1 px-2 rounded-full hover:bg-slate-100 dark:hover:bg-white/10 text-ink transition-all cursor-pointer"
>
<Pin size={11} className={note.isPinned ? "fill-amber-500 text-amber-500" : ""} />
</button>
<button
onClick={(e) => {
e.stopPropagation();
onDeleteNote(note.id);
}}
className="p-1 px-2 rounded-full hover:bg-rose-50 text-rose-500 transition-all cursor-pointer"
>
<Trash2 size={11} />
</button>
</div>
</div>
</div>
</motion.div>
);
})}
</div>
{sortedNotes.length === 0 && (
<div className="h-64 flex flex-col items-center justify-center text-center space-y-4">
<p className="font-serif text-xl italic text-muted-ink">This notebook is waiting for its first vision.</p>
<button
onClick={() => setShowNewNoteModal(true)}
className="px-6 py-2 border border-ink text-[13px] uppercase tracking-[0.2em] hover:bg-ink hover:text-paper transition-all cursor-pointer"
>
Begin Drawing
</button>
</div>
)}
</div>
) : layoutMode === 'table' ? (
// ================== STRUCTURING TABLE VIEW ==================
<div className="max-w-6xl mx-auto space-y-4 select-none">
<div className="w-full overflow-x-auto border border-[#D5D2CD]/40 dark:border-neutral-800/40 rounded-2xl bg-white dark:bg-[#1C1C1E]/20 shadow-sm [&::-webkit-scrollbar]:h-1.5 [&::-webkit-scrollbar-thumb]:bg-black/10 [&::-webkit-scrollbar-track]:bg-transparent">
<table className="w-full text-left border-collapse table-fixed min-w-[700px]">
<thead>
<tr className="border-b border-black/[0.04] dark:border-white/[0.04] bg-black/[0.01] dark:bg-white/[0.01]">
{/* Titre */}
<th
onClick={() => handleSort('title')}
className="w-[40%] px-4 py-3 text-[10px] uppercase tracking-widest font-black text-concrete cursor-pointer hover:text-ink transition-colors"
>
<div className="flex items-center gap-1">
<span>Titre</span>
{renderSortIndicator('title')}
</div>
</th>
{/* Carnet */}
<th
onClick={() => handleSort('carnet')}
className="w-[15%] px-4 py-3 text-[10px] uppercase tracking-widest font-black text-concrete cursor-pointer hover:text-ink transition-colors"
>
<div className="flex items-center gap-1">
<span>Carnet</span>
{renderSortIndicator('carnet')}
</div>
</th>
{/* Labels */}
<th
onClick={() => handleSort('labels')}
className="w-[20%] px-4 py-3 text-[10px] uppercase tracking-widest font-black text-concrete cursor-pointer hover:text-ink transition-colors"
>
<div className="flex items-center gap-1">
<span>Labels</span>
{renderSortIndicator('labels')}
</div>
</th>
{/* Tâches */}
<th
onClick={() => handleSort('tasks')}
className="w-[12%] px-4 py-3 text-[10px] uppercase tracking-widest font-black text-concrete cursor-pointer hover:text-ink transition-colors"
>
<div className="flex items-center gap-1">
<span>Tâches</span>
{renderSortIndicator('tasks')}
</div>
</th>
{/* Modifié */}
<th
onClick={() => handleSort('modified')}
className="w-[13%] px-4 py-3 text-[10px] uppercase tracking-widest font-black text-concrete cursor-pointer hover:text-ink transition-colors"
>
<div className="flex items-center gap-1">
<span>Modifié</span>
{renderSortIndicator('modified')}
</div>
</th>
</tr>
</thead>
<tbody className="divide-y divide-black/[0.03] dark:divide-white/[0.03]">
{sortedNotes.map((note) => {
const carnet = carnets?.find(c => c.id === note.carnetId);
const noteColor = carnet ? getCarnetColor(carnet) : { bg: 'bg-zinc-500/5', border: 'border-zinc-500/10', text: 'text-zinc-500' };
const stats = getNoteTasksStats(note.content);
return (
<tr
key={note.id}
onClick={() => setActiveNoteId(note.id)}
className="h-10 hover:bg-neutral-50 dark:hover:bg-white/[0.02] cursor-pointer transition-all duration-150 relative group"
>
{/* Titre */}
<td className="px-4 py-2 font-serif text-[13px] font-medium text-ink truncate">
<div className="flex items-center gap-2 truncate">
{note.isPinned && <Pin size={11} className="text-amber-500 fill-amber-500 shrink-0" />}
<span className="truncate group-hover:text-accent transition-colors">{note.title || "Note sans titre"}</span>
</div>
</td>
{/* Carnet */}
<td className="px-4 py-2">
{carnet ? (
<span className={`inline-block px-2 py-0.5 rounded-full text-[9px] font-bold tracking-wide border truncate max-w-[125px] ${noteColor.bg} ${noteColor.border} ${noteColor.text}`}>
{carnet.name}
</span>
) : (
<span className="text-zinc-500 text-[10px]">—</span>
)}
</td>
{/* Labels */}
<td className="px-4 py-2">
<div className="flex items-center gap-1 flex-wrap">
{note.tags?.slice(0, 3).map(tag => (
<span
key={tag.id}
className={`px-1.5 py-0.5 rounded text-[8px] font-extrabold uppercase tracking-wider border shrink-0
${tag.type === 'ai'
? 'bg-accent/5 border-accent/15 text-accent'
: 'bg-concrete/5 border-border text-concrete'}`}
>
{tag.label}
</span>
))}
{note.tags && note.tags.length > 3 && (
<span className="text-[8.5px] font-mono text-concrete font-bold shrink-0 bg-neutral-100 dark:bg-neutral-800 px-1 py-0.5 rounded">
+{note.tags.length - 3}
</span>
)}
</div>
</td>
{/* Tâches */}
<td className="px-4 py-2 font-mono text-[10.5px] font-bold text-ink">
{stats.total > 0 ? (
<span className={`inline-flex items-center gap-1 ${stats.completed === stats.total ? 'text-emerald-600 dark:text-emerald-400 font-extrabold' : 'text-concrete'}`}>
{stats.completed}/{stats.total} <span className="text-[9px] font-sans">✓</span>
</span>
) : (
<span className="text-concrete/40">—</span>
)}
</td>
{/* Modifié */}
<td className="px-4 py-2 text-[10.5px] font-mono text-concrete shrink-0 truncate">
{getRelativeFrenchDate(note.date)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
{sortedNotes.length === 0 && (
<div className="h-64 flex flex-col items-center justify-center text-center space-y-4">
<p className="font-serif text-xl italic text-muted-ink">This notebook is waiting for its first vision.</p>
<button
onClick={() => setShowNewNoteModal(true)}
className="px-6 py-2 border border-ink text-[13px] uppercase tracking-[0.2em] hover:bg-ink hover:text-paper transition-all cursor-pointer"
>
Begin Drawing
</button>
</div>
)}
</div>
) : (
// ================== ORIGINAL DETAILED EDITORIAL LIST VIEW ==================
<div className="max-w-3xl space-y-16">
{filteredNotes.map((note, index) => (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 * index, duration: 0.8 }}
key={note.id}
className="space-y-4 group cursor-pointer relative"
onClick={() => setActiveNoteId(note.id)}
>
<h2 className="text-2xl font-serif font-medium text-ink flex items-center justify-between">
<span className="flex items-center gap-3">
{note.isPinned && <Pin size={18} className="text-amber-500 fill-amber-500" />}
{note.title}
</span>
<div className="flex items-center gap-2">
<button
onClick={(e) => {
e.stopPropagation();
onBrainstormNote(note);
}}
className="p-2 rounded-full opacity-0 group-hover:opacity-60 hover:opacity-100 hover:bg-ochre/10 text-ochre transition-all cursor-pointer"
title="Brainstorm this concept"
>
<Wind size={16} />
</button>
<button
onClick={(e) => {
e.stopPropagation();
togglePin(note.id);
}}
className={`p-2 rounded-full transition-all cursor-pointer ${note.isPinned ? 'text-amber-600 bg-amber-50' : 'opacity-0 group-hover:opacity-60 hover:bg-slate-100 dark:hover:bg-white/10 text-ink'}`}
>
<Pin size={16} />
</button>
<button
onClick={(e) => {
e.stopPropagation();
onDeleteNote(note.id);
}}
className="p-2 rounded-full opacity-0 group-hover:opacity-60 hover:opacity-100 hover:bg-rose-50 text-rose-500 transition-all cursor-pointer"
>
<Trash2 size={16} />
</button>
<button className="opacity-0 group-hover:opacity-40 transition-opacity">
<ChevronRight size={20} />
</button>
</div>
</h2>
<div className="flex flex-col md:flex-row gap-8 items-start">
<div className="w-full md:w-56 aspect-[4/3] bg-white/50 dark:bg-white/5 border border-border overflow-hidden rounded shadow-sm flex-shrink-0">
<img
src={note.imageUrl || "https://images.unsplash.com/photo-1503387762-592dea58ef23"}
alt={note.title}
className="w-full h-full object-cover mix-blend-multiply dark:mix-blend-normal opacity-80 grayscale contrast-125 group-hover:grayscale-0 group-hover:opacity-100 transition-all duration-500"
referrerPolicy="no-referrer"
/>
</div>
<div className="space-y-3">
<div className="flex flex-wrap gap-2 mb-2">
{note.tags?.map(tag => (
<div
key={tag.id}
className={`px-2 py-0.5 rounded text-[9px] font-bold uppercase tracking-wider border flex items-center gap-1.5
${tag.type === 'ai'
? 'bg-accent/5 border-accent/20 text-accent'
: 'bg-concrete/5 border-border text-concrete'}`}
>
{tag.type === 'ai' && <Sparkles size={8} />}
{tag.label}
</div>
))}
</div>
<p className="text-[14px] leading-relaxed text-ink/80 font-light max-w-lg text-justify line-clamp-4">
{note.content}
</p>
<span className="text-[11px] text-muted-ink uppercase tracking-widest font-medium">Read more</span>
</div>
</div>
</motion.div>
))}
{filteredNotes.length === 0 && (
<div className="h-64 flex flex-col items-center justify-center text-center space-y-4">
<p className="font-serif text-xl italic text-muted-ink">This notebook is waiting for its first vision.</p>
<button
onClick={() => setShowNewNoteModal(true)}
className="px-6 py-2 border border-ink text-[13px] uppercase tracking-[0.2em] hover:bg-ink hover:text-paper transition-all cursor-pointer"
>
Begin Drawing
</button>
</div>
)}
</div>
)}
</div>
<footer className="px-12 py-6 border-t border-ink/5 text-center mt-auto">
<p className="text-[11px] text-muted-ink uppercase tracking-[0.2em] font-medium">
&copy; 2024 Architectural Grid. All rights reserved.
</p>
</footer>
</div>
);
}
return (
<div className="h-full flex overflow-hidden transition-all duration-500">
<div className="flex-1 flex flex-col overflow-y-auto bg-white dark:bg-paper">
<div className="px-6 sm:px-12 py-8 flex items-center justify-between sticky top-0 bg-white/90 dark:bg-paper/90 backdrop-blur-sm z-40 border-b border-border gap-4">
<div className="flex items-center gap-2 sm:gap-4">
<button
onClick={onOpenSidebar}
className="lg:hidden p-2 -ml-2 text-ink hover:bg-black/5 rounded-lg transition-colors"
>
<Menu size={20} />
</button>
<button
onClick={() => setActiveNoteId(null)}
className="flex items-center gap-2 text-ink hover:opacity-60 transition-opacity"
title="Retour aux documents"
>
<ArrowLeft size={18} />
</button>
</div>
<div className="flex items-center gap-2 sm:gap-4 overflow-x-auto no-scrollbar">
<button
onClick={() => fileInputRef.current?.click()}
className="flex items-center gap-2 px-3 py-1.5 rounded-full border border-border text-muted-ink hover:text-ink transition-all"
title="Add attachment"
>
<Paperclip size={16} />
<input
type="file"
ref={fileInputRef}
onChange={handleFileUpload}
className="hidden"
accept=".pdf,.docx,.txt"
/>
</button>
<button
onClick={() => onBrainstormNote(activeNote!)}
className="p-2 rounded-full border border-ochre/30 text-ochre hover:bg-ochre/5 transition-all cursor-pointer"
title="Brainstorm IA"
>
<Wind size={16} />
</button>
<button
onClick={() => onGenerateFlashcards?.(activeNote!.id)}
disabled={isGeneratingFlashcards}
className="p-2 rounded-full border border-accent/30 text-accent hover:bg-accent/5 transition-all disabled:opacity-50 cursor-pointer"
title={isGeneratingFlashcards ? 'Génération...' : 'Révisions & Flashcards'}
>
{isGeneratingFlashcards ? (
<div className="w-4 h-4 rounded-full border-2 border-accent border-t-transparent animate-spin" />
) : (
<GraduationCap size={16} />
)}
</button>
<button
onClick={() => {
if (isEditing && activeNote) {
// Save edits
const updatedNote = {
...activeNote,
title: titleInputRef.current?.value || activeNote.title,
content: contentTextareaRef.current?.value || activeNote.content
};
onUpdateNote?.(updatedNote);
}
setIsEditing(!isEditing);
}}
className={`p-2 rounded-full border transition-all duration-300
${isEditing ? 'bg-accent text-white border-accent shadow-lg shadow-accent/20' : 'border-border text-ink hover:bg-slate-50'}`}
title={isEditing ? 'Visualiser' : 'Modifier'}
>
{isEditing ? <Eye size={16} /> : <Edit3 size={16} />}
</button>
<button
onClick={() => togglePin(activeNoteId!)}
className={`p-2 rounded-full transition-all ${activeNote?.isPinned ? 'text-amber-600 bg-amber-50 dark:bg-ochre/10' : 'text-muted-ink hover:text-ink'}`}
title={activeNote?.isPinned ? "Unpin note" : "Pin note"}
>
<Pin size={18} className={activeNote?.isPinned ? 'fill-amber-600' : ''} />
</button>
{/* Native simulated connection status indicator toggle */}
<button
onClick={() => {
window.dispatchEvent(new CustomEvent('toggle-websocket-simulate'));
}}
className={`p-2 rounded-full transition-all shrink-0 border cursor-pointer
${wsConnected
? 'bg-blue-500/5 text-blue-600 border-blue-500/10 hover:bg-rose-500/10 hover:text-rose-600 hover:border-rose-500/15'
: 'bg-amber-500/5 text-amber-600 border-amber-500/10 hover:bg-blue-500/10 hover:text-blue-600 hover:border-blue-500/15'}`}
title={wsConnected ? "WebSocket : En ligne (Cliquez pour déconnecter)" : "WebSocket : Hors ligne (Cliquez pour reconnecter)"}
>
<span className={`w-1.5 h-1.5 rounded-full ${wsConnected ? 'bg-blue-500 animate-pulse' : 'bg-amber-500'}`} />
</button>
<button
onClick={() => setIsAISidebarOpen(!isAISidebarOpen)}
className={`p-2 rounded-full border transition-all duration-300
${isAISidebarOpen ? 'bg-ink text-paper border-ink' : 'border-border text-ink hover:bg-white/50 dark:hover:bg-white/5'}`}
title="Assistant IA"
>
<Sparkles size={16} />
</button>
<button className="p-2 text-muted-ink hover:text-red-500 transition-colors">
<Trash2 size={18} />
</button>
<button className="p-2 text-muted-ink hover:text-ink transition-colors">
<MoreVertical size={18} />
</button>
<button
onClick={() => setIsNoteInfoOpen(!isNoteInfoOpen)}
className={`p-2 rounded-full border transition-all duration-300 cursor-pointer shrink-0
${isNoteInfoOpen ? 'bg-ink text-paper border-ink dark:bg-white dark:text-ink' : 'border-border text-ink hover:bg-white/50 dark:hover:bg-white/5'}`}
title="Informations & Versions de la note"
>
<PanelRight size={16} />
</button>
</div>
</div>
<div className="max-w-4xl mx-auto w-full px-12 py-16 space-y-12 relative">
<AnimatePresence>
{slashMenu?.isOpen && (
<SlashMenu
position={{ top: slashMenu.top, left: slashMenu.left }}
onSelect={(type) => insertCommand(type)}
onClose={() => setSlashMenu(null)}
/>
)}
</AnimatePresence>
{activeNote?.isClipped && (
<div className="w-full relative overflow-hidden bg-cyan-500/10 dark:bg-cyan-500/5 rounded-xl border border-cyan-500/20 p-4 space-y-2">
<div className="absolute top-0 left-0 right-0 h-[8px] bg-cyan-600/30" />
<div className="flex flex-wrap items-center justify-between gap-4 text-xs pt-1.5 select-all">
<div className="flex items-center gap-2.5 min-w-0">
{activeNote.clipFavicon ? (
<img
src={activeNote.clipFavicon}
alt="favicon"
className="w-4.5 h-4.5 object-contain rounded bg-white p-0.5 border border-cyan-500/10"
onError={(e) => { (e.target as any).src = 'https://www.google.com/s2/favicons?domain=google.com'; }}
/>
) : (
<span className="p-1 bg-cyan-500/5 rounded"><Globe size={13} className="text-cyan-600 dark:text-cyan-400" /></span>
)}
<span className="text-[11px] font-mono text-cyan-800 dark:text-cyan-300 truncate max-w-[400px]" title={activeNote.clipSourceUrl}>
{activeNote.clipSourceUrl}
</span>
</div>
<div className="flex items-center gap-2 text-[10px] text-cyan-700/80 dark:text-cyan-400/80 font-medium">
<span className="bg-cyan-500/10 dark:bg-cyan-500/20 px-2 py-0.5 rounded text-[9.5px] tracking-wide uppercase font-bold text-cyan-700 dark:text-cyan-300">
Source web
</span>
<span>Capturé : {activeNote.clipDate || activeNote.date}</span>
</div>
</div>
</div>
)}
<div className="space-y-4">
<div className="flex items-center gap-3 text-[12px] text-muted-ink uppercase tracking-[.25em] font-bold flex-wrap">
<span className="text-accent">{activeCarnet?.name}</span>
<ChevronRight size={10} className="text-concrete" />
<span className="text-concrete">{activeNote?.date}</span>
{activeNote?.isClipped && (
<>
<ChevronRight size={10} className="text-concrete" />
<span className="text-cyan-600 dark:text-cyan-400 bg-cyan-500/10 px-1.5 py-0.5 rounded text-[9.5px] tracking-widest">Source web</span>
</>
)}
</div>
{isEditing ? (
<input
type="text"
ref={titleInputRef}
defaultValue={activeNote?.title}
className="w-full text-5xl md:text-6xl font-serif font-bold text-ink leading-tight bg-transparent border-none outline-none focus:ring-0 placeholder:text-concrete/20"
placeholder="Titre de la note..."
/>
) : (
<h1 className="text-5xl md:text-6xl font-serif font-bold text-ink leading-tight">
{activeNote?.title}
</h1>
)}
<div className="flex flex-wrap gap-2 pt-2">
{activeNote?.tags?.map(tag => (
<div
key={tag.id}
className={`px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-widest border flex items-center gap-2
${tag.type === 'ai'
? 'bg-accent/5 border-accent/20 text-accent'
: 'bg-paper border-border text-concrete'}`}
>
{tag.type === 'ai' && <Sparkles size={12} />}
{tag.label}
{tag.type === 'ai' && (
<div className="w-1.5 h-1.5 rounded-full bg-accent animate-pulse" />
)}
</div>
))}
</div>
{/* Flashcards alert state indicator (E) */}
{activeNote && noteFlashcards.length > 0 && (
<div id="note-flashcard-alert" className="flex flex-wrap items-center gap-4 text-xs font-semibold bg-sage/10 text-sage dark:bg-sage/5 border border-sage/25 rounded-2xl px-4 py-2.5 w-fit mt-3 select-none">
<div className="flex items-center gap-1.5">
<GraduationCap size={15} className="text-sage" />
<span>{noteFlashcards.length} flashcards générées · Prochain rappel : {nextRecallText}</span>
</div>
<button
onClick={() => onTriggerReviewDeck?.(activeNote.id)}
className="text-accent hover:underline font-bold text-xs shrink-0 cursor-pointer"
>
Réviser maintenant
</button>
</div>
)}
</div>
<div className="aspect-[16/9] w-full bg-slate-100 dark:bg-white/5 rounded-xl overflow-hidden shadow-2xl relative group/img">
<img
src={activeNote?.imageUrl}
alt={activeNote?.title}
className="w-full h-full object-cover transition-transform duration-700 group-hover/img:scale-105"
referrerPolicy="no-referrer"
/>
<div className="absolute inset-0 bg-gradient-to-t from-ink/20 to-transparent pointer-events-none" />
</div>
<div className="max-w-2xl mx-auto w-full space-y-8 pb-40">
{activeNote?.attachments && activeNote.attachments.length > 0 && (
<div className="space-y-4">
<h4 className="text-[10px] uppercase font-bold tracking-widest text-concrete border-b border-border pb-2">Pièces jointes ({activeNote.attachments.length})</h4>
<div className="grid grid-cols-2 gap-4">
{activeNote.attachments.map(att => (
<div key={att.id} className="p-3 border border-border rounded-xl bg-white/40 flex items-center justify-between group">
<div className="flex items-center gap-3">
<div className="p-2 bg-accent/10 text-accent rounded-lg">
<FileText size={16} />
</div>
<div className="overflow-hidden">
<p className="text-xs font-bold text-ink truncate w-32">{att.name}</p>
<p className="text-[9px] uppercase font-medium text-concrete">{att.type}</p>
</div>
</div>
<div className="flex items-center gap-1">
{isAnalyzing === att.id ? (
<Loader2 size={14} className="animate-spin text-accent" />
) : (
<button
onClick={() => setActiveDocQnA(att)}
className="p-2 hover:bg-accent/10 text-accent rounded-lg transition-colors opacity-0 group-hover:opacity-100"
title="Converser avec ce document"
>
<MessageSquare size={14} />
</button>
)}
</div>
</div>
))}
</div>
</div>
)}
{activeNote && (
<ModernBlockNoteEditor
note={activeNote}
onUpdateNote={onUpdateNote || (() => {})}
allNotes={allNotes}
/>
)}
<div className="space-y-12 mt-12">
{/* Memory Echo Section */}
{memoryEchoBlock && (
<div className="p-6 rounded-2xl bg-gradient-to-br from-indigo-500/[0.03] via-transparent to-transparent border border-indigo-500/10 shadow-sm space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Sparkles size={14} className="text-indigo-500 animate-pulse" />
<span className="text-[10px] uppercase font-bold tracking-widest text-indigo-500">Memory Echo (Affinité Sémantique)</span>
</div>
<span className="text-[10px] font-mono font-bold text-indigo-600 dark:text-indigo-400 bg-indigo-50 dark:bg-indigo-950/40 px-2 py-0.5 rounded">
92% affinité sémétrique
</span>
</div>
<p className="text-sm font-serif italic text-ink/80 leading-relaxed pl-3 border-l pb-1 border-indigo-500/20">
« {memoryEchoBlock.text.substring(0, 150)}... »
</p>
<div className="flex items-center justify-between text-[11px] pt-1 border-t border-black/[0.03] dark:border-white/[0.02] flex-wrap gap-2">
<span className="text-concrete">Passage détecté dans : <strong className="text-ink/60 font-semibold">{memoryEchoBlock.noteTitle}</strong></span>
<div className="flex items-center gap-3">
<button
onClick={() => setActiveNoteId(memoryEchoBlock.noteId)}
className="text-concrete hover:text-ink font-bold transition-all hover:underline"
>
Voir la connexion
</button>
<button
onClick={() => {
setPrefilledBlock({ noteId: memoryEchoBlock.noteId, blockIndex: memoryEchoBlock.blockIndex });
setIsPickerOpen(true);
}}
className="flex items-center gap-1.5 text-blue-600 dark:text-blue-400 font-extrabold hover:underline"
>
<Link2 size={12} />
Embedder ce passage
</button>
</div>
</div>
</div>
)}
{/* Backlinks panel */}
{backlinks.length > 0 && (
<div className="p-6 rounded-2xl bg-[#FCFBFA] dark:bg-zinc-900/[0.15] border border-[#D5D2CD]/40 dark:border-neutral-800/40 space-y-4 shadow-[0_1px_2px_rgba(0,0,0,0.01)]">
<div className="flex items-center gap-2 pb-2 border-b border-black/[0.03] dark:border-white/[0.02]">
<div className="w-1.5 h-3 bg-accent rounded-full animate-pulse" />
<span className="text-[10px] uppercase font-bold tracking-widest text-concrete">Rétroliens &amp; Intégrations Sémantiques</span>
</div>
<div className="space-y-3">
<span className="text-xs font-semibold text-ink/80 block">Embeddé comme Living Block dans {backlinks.length} note{backlinks.length > 1 ? 's' : ''} :</span>
<div className="grid grid-cols-1 gap-2.5">
{backlinks.map(({ note, accessedAt }) => (
<div
key={note.id}
onClick={() => 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"
>
<div className="flex items-center gap-3">
<Link2 size={13} className="text-blue-500 shrink-0" />
<div className="min-w-0">
<span className="text-xs font-bold text-ink dark:text-dark-ink truncate block group-hover:text-accent transition-colors">
{note.title || "Note sans titre"}
</span>
<span className="text-[9.5px] font-mono uppercase tracking-wider text-concrete">
Carnet : {carnets?.find(c => c.id === note.carnetId)?.name || "Général"}
</span>
</div>
</div>
<span className="text-[10px] text-concrete font-medium bg-black/[0.03] dark:bg-white/5 py-0.5 px-3 rounded-full">
Accès : {accessedAt}
</span>
</div>
))}
</div>
</div>
</div>
)}
</div>
</div>
</div>
</div>
{/* Document Q&A Overlay */}
<AnimatePresence>
{activeDocQnA && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-8 bg-ink/30 backdrop-blur-md">
<motion.div
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.95, opacity: 0 }}
className="w-full max-w-4xl h-full bg-paper dark:bg-dark-paper border border-border rounded-[32px] shadow-2xl flex overflow-hidden"
>
{/* Document Preview (Mock) */}
<div className="flex-1 bg-slate-50 dark:bg-black/20 border-r border-border p-12 overflow-y-auto custom-scrollbar">
<div className="flex items-center gap-4 mb-8">
<div className="p-3 bg-accent text-white rounded-xl">
<FileText size={24} />
</div>
<div>
<h3 className="text-xl font-serif font-bold text-ink">{activeDocQnA.name}</h3>
<p className="text-[10px] uppercase font-bold tracking-widest text-concrete">DOCUMENT SOURCE ANALYSÉ</p>
</div>
</div>
<div className="prose prose-sm prose-slate dark:prose-invert max-w-none">
<div className="whitespace-pre-wrap font-serif text-lg leading-relaxed text-ink/80 italic">
{activeDocQnA.content || "Analyse du contenu en cours..."}
</div>
</div>
</div>
{/* Chat Side */}
<div className="w-[400px] flex flex-col bg-white dark:bg-paper">
<div className="p-6 border-b border-border flex items-center justify-between">
<div className="flex items-center gap-2">
<Sparkles size={18} className="text-accent" />
<h4 className="text-xs font-bold uppercase tracking-widest text-ink">Expert Document</h4>
</div>
<button onClick={() => setActiveDocQnA(null)} className="p-2 hover:bg-slate-50 dark:hover:bg-white/5 rounded-full text-concrete">
<X size={20} />
</button>
</div>
<div className="flex-1 p-6 overflow-y-auto space-y-6">
<div className="p-4 bg-accent/5 border border-accent/10 rounded-2xl">
<p className="text-xs text-accent font-medium leading-relaxed">
Bonjour ! J'ai analysé ce document. Posez-moi n'importe quelle question sur son contenu, les chiffres clés ou les concepts abordés.
</p>
</div>
</div>
<div className="p-6 border-t border-border">
<div className="relative">
<textarea
placeholder="Poser une question au document..."
className="w-full bg-slate-50 dark:bg-black/20 border border-border rounded-xl p-4 pr-12 text-xs outline-none focus:border-accent transition-all resize-none"
rows={3}
/>
<button className="absolute right-3 bottom-3 p-2 bg-accent text-white rounded-lg shadow-lg shadow-accent/10">
<Sparkles size={16} />
</button>
</div>
</div>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
<BlockPicker
isOpen={isPickerOpen}
onClose={() => setIsPickerOpen(false)}
currentNote={activeNote}
allNotes={allNotes}
carnets={carnets}
onSelectBlock={handleSelectBlock}
prefilledBlock={prefilledBlock}
/>
<NotebookInfoSidebar
isOpen={isNoteInfoOpen}
onClose={() => setIsNoteInfoOpen(false)}
activeNote={activeNote}
notes={allNotes}
carnets={carnets}
onOpenNote={setActiveNoteId}
onUpdateNote={onUpdateNote}
/>
</div>
);
};