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>
1533 lines
72 KiB
TypeScript
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">
|
|
© 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 & 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>
|
|
);
|
|
};
|