--- stepsCompleted: [1, 2, 3, 4] inputDocuments: - notebooks-contextual-labels-prd.md - notebooks-wireframes.md - project-context.md workflowType: 'architecture' project_name: 'Keep - Notebooks & Labels Contextuels' user_name: 'Ramez' date: '2026-01-11' communication_language: 'French' document_output_language: 'English' focusArea: 'Notebooks with Contextual Labels Feature' --- # Architecture Decision Document - Notebooks & Labels Contextuels _This document builds collaboratively through step-by-step discovery. Sections are appended as we work through each architectural decision together._ --- ## Project Context Analysis ### Requirements Overview **Functional Requirements:** Le projet **Notebooks & Labels Contextuels** est une refonte architecturale majeure du système d'organisation de Keep, transformant un modèle de tags plat en une structure hiérarchique contextuelle avec intelligence artificielle intégrée. **1. Structure en Notebooks** - Notebooks comme organisation principale (remplacement/évolution des tags globaux) - Notes générales (Inbox) pour notes non organisées - Une note appartient à UN seul notebook (ou aucune) - Labels 100% contextuels à chaque notebook (isolement total) **2. UX Interactions Complexes** - Drag & drop à deux niveaux : réorganiser les notebooks + déplacer des notes - Menu contextuel alternatif pour le déplacement - Création contextuelle de notebooks et labels - Navigation fluide entre Inbox et Notebooks **3. Intégration IA (6 Features)** - **IA1:** Suggestion automatique de notebook (quand note créée dans Inbox) - **IA2:** Suggestion de labels contextuels (filtrés par notebook actif) - **IA3:** Organisation batch intelligente (Inbox → Notebooks) - **IA4:** Création automatique de labels (détection thèmes récurrents) - **IA5:** Recherche sémantique contextuelle (limitée au notebook) - **IA6:** Synthèse automatique par notebook **4. Migration Brownfield** - Migration douce depuis le système de tags plat existant - Notebook par défaut pour préserver les labels existants - ZÉRO breaking changes - le système actuel continue de fonctionner **Non-Functional Requirements:** Les NFRs critiques qui façonneront l'architecture : **Performance:** - Drag & drop en temps réel (< 100ms latence perçue) - Recherche sémantique < 300ms pour 1000 notes (existant) - Suggestions IA < 2s (existant) - Chargement de sidebar instantané (< 200ms) **Sécurité & Intégrité:** - Labels isolés par notebook (pas de fuite de données entre notebooks) - Cascade delete maîtrisé : notebook supprimé → notes → Inbox (pas de perte de données) - Contrainte d'unicité : `@@unique([notebookId, name])` pour les labels **Brownfield Compatibility:** - **ZERO breaking changes** - le système de tags actuel continue de fonctionner - `notebookId` optionnel sur Note (null = Inbox) - `notebookId` obligatoire sur Label (migration avec valeur par défaut 'TEMP_MIGRATION') - Rollback possible si nécessaire **Scalability:** - Supporte jusqu'à 100+ notebooks par utilisateur - Supporte jusqu'à 50+ labels par notebook - Performance dégradée gracieusement avec 1000+ notes ### Scale & Complexity **Project Complexity: MEDIUM-HIGH** **Complexity Indicators:** - ✅ Real-time features (drag & drop synchronisé à deux niveaux) - ✅ IA multi-facettes (6 features distinctes réutilisant l'infrastructure existante) - ✅ Migration de données brownfield (système de tags vers notebooks) - ❌ PAS de multi-tenancy (single-user only - simplifie l'architecture) - ❌ PAS de compliance réglementaire spécifique (GDPR basique uniquement) - ✅ Intégration backend complexe (Prisma + IA) **Primary Technical Domain:** Full-stack Web with AI - **Frontend:** React/Next.js avec interactions riches (sidebar, drag & drop, modals) - **Backend:** Server Actions + API Routes (existants à étendre) - **Database:** Prisma ORM + SQLite (existants) - **AI:** OpenAI/Ollama (infrastructure existante à adapter pour être contextuelle) **Estimated Architectural Components:** ~8-10 composants majeurs 1. Notebook Management (CRUD + ordre manuel) 2. Label Management (contextuel par notebook) 3. Drag & Drop System (deux niveaux) 4. IA Suggestion Engine (adaptation existante) 5. Migration Service (tags → notebooks) 6. Search Contextualization (limitation par notebook) 7. State Management (sidebar + grid + optimistic UI) 8. Inbox/General Notes (zone temporaire) ### Technical Constraints & Dependencies **Contraintes Existantes:** - **Next.js 16.1.1** avec App Router (doit être compatible) - **Prisma 5.22.0** + SQLite (doit étendre le schema existant) - **React 19.2.3** strict mode (doit respecter les patterns existants) - **Muuri** pour Masonry grid (doit cohabiter avec le nouveau drag & drop) - **Server Actions** + **revalidatePath** (pattern existant à respecter) **Dépendances IA:** - **Vercel AI SDK 6.0.23** (déjà en place) - **Auto-tagging system** (existant - à adapter pour être contextuel) - **Semantic search** (existant - à limiter par notebook) - **Language detection** (existant - à réutiliser) **Contraintes UX:** - **Responsive design** (desktop priorité, mobile secondaire) - **Accessibilité WCAG AA** (existant) - **Dark mode** (existant) - **Performance targets** (existant - à maintenir) ### Cross-Cutting Concerns Identified **1. State Management & Synchronization** - **Challenge:** État global distribué (sidebar + grid + modals + IA) - **Impact:** Nécessite une architecture d'état cohérente pour éviter les incohérences - **Patterns:** React Context + useOptimistic + Server Actions (existants) **2. Drag & Drop à Deux Niveaux** - **Challenge:** Unifier le drag & drop Muuri (existant) avec le drag & drop notebooks (nouveau) - **Impact:** UX cohérente pour l'utilisateur, code maintenable - **Patterns:** @dnd-kit ou Muuri natif (à décider) **3. IA Contextuelle** - **Challenge:** Adapter l'auto-tagging existant pour filtrer par notebook - **Impact:** Réutilisation maximale du code existant, pas de duplication - **Patterns:** Services IA avec paramètre `notebookId` **4. Migration de Données** - **Challenge:** Migrer les tags globaux vers les notebooks sans perte - **Impact:** User experience douce, pas de données perdues - **Patterns:** Migration script + notebook par défaut + IA-assistée **5. Performance de la Recherche** - **Challenge:** Recherche sémantique limitée au notebook actif - **Impact:** Résultats pertinents, performance acceptable - **Patterns:** Filtrage au niveau Prisma query (pas post-traitement) **6. Undo/Redo pour Actions Déstructives** - **Challenge:** Permettre d'annuler les déplacements de notes, suppressions de notebooks - **Impact:** User confiance, UX sans peur - **Patterns:** Historique des actions ou Server Actions avec rollback --- ## Step 3: Library & Technology Evaluation (Brownfield Adaptation) ### Brownfield Context Ce projet N'est PAS greenfield - Keep existe déjà avec une stack mature et des drag & drop fonctionnels. Cette section évalue les technologies existantes et identifie ce qui doit être ajouté. ### Existing Technology Stack **✅ Technologies déjà en place (à réutiliser) :** | Component | Technology | Status | Usage dans le projet Notebooks | |-----------|-----------|--------|-------------------------------| | Masonry Layout + Drag & Drop | **Muuri** | ✅ En production | Réorganisation des notes (niveau 1) + Réorganisation notebooks (niveau 2 - NOUVEAU) | | AI SDK | **Vercel AI SDK 6.0.23** | ✅ En production | Adaptation pour IA contextuelle par notebook | | Database ORM | **Prisma 5.22.0** | ✅ En production | Extension du schema avec Notebook + Label | | Server Actions | **Next.js Server Actions** | ✅ En production | CRUD operations pour notebooks/labels | | Auto-tagging IA | **Custom implementation** | ✅ En production | Adapter pour filtrer par notebookId | | Semantic Search | **Embeddings + vector search** | ✅ En production | Limiter scope au notebook actif | | State Management | **React Context + useOptimistic** | ✅ En production | Étendre pour gérer état notebooks | | Framework | **Next.js 16.1.1 + React 19.2.3** | ✅ En production | Base de développement | | Database | **SQLite** | ✅ En production | Ajout tables Notebook, Label | ### 🔑 DECISION ARCHITECTURALE CRITIQUE #1: Drag & Drop Unifié **Question:** Comment implémenter le drag & drop pour les deux niveaux (notes + notebooks) ? **Option Évaluée:** @dnd-kit - ❌ **REJETÉ** - Introduirait une deuxième bibliothèque de drag & drop - ❌ Dupliquerait les dépendances - ❌ UX potentiellement incohérente (deux "feels" différents) - ✅ Modern mais déjà disponible dans Muuri **DÉCISION RETENUE: Utiliser Muuri pour les DEUX niveaux de drag & drop** | Niveau | Usage | Implémentation Muuri | |--------|-------|---------------------| | **Niveau 1** | Réorganisation des notes dans la masonry grid | ✅ **DÉJÀ FONCTIONNEL** (existant) | | **Niveau 2** | Réorganisation des notebooks dans la sidebar | 🆕 **À IMPLÉMENTER** avec Muuri | **Justification:** 1. **Single Library Principle** - Une seule bibliothèque de drag & drop à maîtriser 2. **Consistance UX** - Même comportement visuel et interactif partout dans l'app 3. **Moins de dépendances** - Pas de @dnd-kit, @hello-pangea/dnd, ou react-beautiful-dnd 4. **Performance** - Muuri est déjà optimisé pour le projet (déjà testé en production) 5. **Code maintenance** - Un seul pattern de drag & drop à maintenir **Implémentation Technique:** ```typescript // Niveau 1: Existant (masonry-grid.tsx) // Déjà implémenté avec Muuri - PAS DE CHANGEMENT // Niveau 2: Nouveau (notebooks-sidebar.tsx) // À implémenter avec la même configuration Muuri const notebooksMuuri = new Muuri(notebooksGridRef.current, { dragEnabled: true, dragContainer: document.body, dragStartPredicate: { distance: 5, delay: 0 }, // Même configuration que masonry-grid pour consistance }) ``` ### Nouvelles Dépendances Requises **✅ AUCUNE nouvelle dépendance majeure requise** Ce projet va être implémenté avec 100% des technologies existantes. Les seuls ajouts potentiels sont : | Type | Dépendance | Raison | Taille | |------|-----------|--------|--------| | Icons | `lucide-react` (optionnel) | Icons pour notebooks | ~KB (déjà utilisé dans le projet) | | Utilities | `clsx` / `cn` (optionnel) | Classname utilities | ~KB (peut être déjà là) | **Decision:** Utiliser les utilitaires déjà existants dans le projet. PAS de nouvelles dépendances npm majeures. --- ## Step 4: Architectural Decisions Cette section documente TOUTES les décisions architecturales critiques pour chaque composant majeur du projet. ### 🔑 DECISION ARCHITECTURALE #2: Database Schema & Data Model **Question:** Comment modéliser les notebooks et labels contextuels dans Prisma ? **Contraintes:** - ZERO breaking changes sur le système existant - Labels doivent être isolés par notebook (pas de fuite) - Support pour "Notes générales" (Inbox) sans notebook - Cascade delete contrôlé (pas de perte de données) **DÉCISION RETENUE: Schéma Prisma Extensible avec Relations Optionnelles** ```prisma // NOUVEAU: Modèle Notebook model Notebook { id String @id @default(cuid()) name String icon String? // Emoji ou icon name color String? // Hex color pour personnalisation order Int // Ordre manuel (drag & drop) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) notes Note[] // Une note peut appartenir à un notebook labels Label[] // Labels sont contextuels à ce notebook createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([userId, order]) @@index([userId]) } // NOUVEAU: Modèle Label (contextuel) model Label { id String @id @default(cuid()) name String color String? notebookId String // OBLIGATOIRE: Label appartient à un notebook notebook Notebook @relation(fields: [notebookId], references: [id], onDelete: Cascade) notes Note[] // Plusieurs notes peuvent avoir ce label createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([notebookId, name]) // Nom unique DANS le notebook @@index([notebookId]) } // MODIFIÉ: Modèle Note existant model Note { id String @id @default(cuid()) title String? content String isPinned Boolean @default(false) size String @default("small") order Int userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) // NOUVEAU: Relation optionnelle avec Notebook notebookId String? // NULL = "Notes générales" (Inbox) notebook Notebook? @relation(fields: [notebookId], references: [id], onDelete: SetNull) // NOUVEAU: Labels contextuels (relation many-to-many) labels Label[] // ... autres champs existants (embeddings, etc.) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([userId, order]) @@index([userId, notebookId]) // Pour filtrer par notebook efficacement } ``` **Justification:** 1. **`notebookId` optionnel sur Note** - Permet "Notes générales" (null = Inbox) 2. **`onDelete: SetNull`** - Notebook supprimé → notes deviennent "générales" (pas de perte) 3. **`@@unique([notebookId, name])`** - Labels uniques par notebook (pas globaux) 4. **Relation many-to-many Note <-> Label** - Prisma gère la table de jointure automatiquement 5. **Indexes sur `[userId, notebookId]`** - Recherche performante par notebook **Migration Strategy:** ```typescript // Migration script (exécuté une fois) export async function migrateToNotebooks() { // 1. Créer notebook "TEMP_MIGRATION" pour tous les labels existants const tempNotebook = await prisma.notebook.create({ data: { name: "Labels Migrés", userId: currentUserId, order: 999, } }) // 2. Attribuer tous les labels existants à ce notebook await prisma.label.updateMany({ data: { notebookId: tempNotebook.id } }) // 3. Laisser les notes existantes sans notebook (Inbox) // Elles pourront être réorganisées plus tard par l'utilisateur } ``` --- ### 🔑 DECISION ARCHITECTURALE #3: State Management Architecture **Question:** Comment gérer l'état distribué (sidebar + grid + modals + IA) ? **Contraintes:** - État global distribué (plusieurs zones de l'UI) - Optimistic UI pour drag & drop (latence perçue < 100ms) - IA suggestions asynchrones - Server Actions pour persistance **DÉCISION RETENUE: React Context + useOptimistic + Server Actions (Hybrid)** **Architecture en Couches:** ``` ┌─────────────────────────────────────────────────────┐ │ LAYER 1: UI State (Local Component State) │ │ - Modal open/close │ │ - Input focus │ │ - Temporary UI states │ └─────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────┐ │ LAYER 2: Optimistic State (useOptimistic) │ │ - Drag & drop order changes │ │ - Note moves between notebooks │ │ - Notebook reordering │ │ - CRUD operations (création/suppression notebooks) │ └─────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────┐ │ LAYER 3: Global State (React Context) │ │ - Current notebook filter │ │ - List of notebooks (sidebar) │ │ - Labels contextuels (par notebook) │ └─────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────┐ │ LAYER 4: Server State (Server Actions + Cache) │ │ - Persistent data (Prisma) │ │ - Next.js cache (revalidatePath) │ │ - IA computations (asynchronous) │ └─────────────────────────────────────────────────────┘ ``` **Implémentation:** ```typescript // app/context/notebooks-context.tsx export type NotebooksContextValue = { // État global notebooks: Notebook[] currentNotebook: Notebook | null // null = "Notes générales" currentLabels: Label[] // Labels du notebook actuel // Actions optimistes createNotebookOptimistic: (data: CreateNotebookInput) => Promise updateNotebookOrderOptimistic: (notebookIds: string[]) => Promise moveNoteToNotebookOptimistic: (noteId: string, notebookId: string | null) => Promise // Actions IA (non-optimistes, asynchrones) suggestNotebookForNote: (noteContent: string) => Promise suggestLabelsForNote: (noteContent: string, notebookId: string) => Promise } export const NotebooksContext = createContext(null) export function useNotebooks() { const context = useContext(NotebooksContext) if (!context) throw new Error('useNotebooks must be used within NotebooksProvider') return context } // app/providers.tsx export function NotebooksProvider({ children }: { children: ReactNode }) { const [notebooks, setNotebooks] = useState([]) const [currentNotebook, setCurrentNotebook] = useState(null) // Charger les notebooks au mount useEffect(() => { loadNotebooks().then(setNotebooks) }, []) // Charger les labels du notebook actuel const currentLabels = useMemo(() => { if (!currentNotebook) return [] return notebooks.find(nb => nb.id === currentNotebook.id)?.labels ?? [] }, [currentNotebook, notebooks]) // Optimistic actions avec useOptimistic const [optimisticNotebooks, addOptimisticNotebook] = useOptimistic( notebooks, (state, newNotebook: Notebook) => [...state, newNotebook] ) const createNotebookOptimistic = async (data: CreateNotebookInput) => { // 1. Optimistic update const tempId = `temp-${Date.now()}` addOptimisticNotebook({ ...data, id: tempId }) // 2. Server Action try { const result = await createNotebook(data) // 3. Revalidation triggers reload with real data revalidatePath('/') } catch (error) { // 4. Error handling - rollback automatic } } return ( {children} ) } ``` **Justification:** 1. **useOptimistic** - React 19 feature native, parfaite pour drag & drop 2. **React Context** - Suffisant pour l'état global (pas besoin de Redux/Zustand) 3. **Server Actions** - Pattern existant à maintenir pour la persistance 4. **Séparation des préoccupations** - Chaque couche a sa responsabilité --- ### 🔑 DECISION ARCHITECTURALE #4: IA Contextuelle Architecture **Question:** Comment adapter l'IA existante pour être contextuelle par notebook ? **Contraintes:** - Réutiliser l'infrastructure existante (Vercel AI SDK) - Filtrer les suggestions par notebook actif - Pas de duplication de code - 6 features IA à implémenter **DÉCISION RETENUE: Pattern Adapter avec Filtrage au Niveau Service** **Architecture IA Contextuelle:** ``` ┌──────────────────────────────────────────────────────┐ │ IA Services Existants (à réutiliser) │ │ - autoTagService.ts (suggestions de tags) │ │ - semanticSearchService.ts (recherche sémantique) │ │ - languageDetectionService.ts (détection langue) │ └──────────────────────────────────────────────────────┘ ↓ ┌──────────────────────────────────────────────────────┐ │ NOUVEAU: Contextual Adapter Layer │ │ - contextualAutoTagService(notebookId) │ │ - contextualSearchService(notebookId) │ │ - notebookSuggestionService() │ └──────────────────────────────────────────────────────┘ ↓ ┌──────────────────────────────────────────────────────┐ │ Server Actions (UI Layer) │ │ - suggestLabels(noteId, notebookId) │ │ - suggestNotebook(noteId) │ │ - organizeInbox(noteIds) │ └──────────────────────────────────────────────────────┘ ``` **Implémentation:** ```typescript // lib/ai/services/contextual-auto-tag.service.ts import { autoTagService } from './auto-tag.service' export class ContextualAutoTagService { constructor(private notebookId: string) {} /** * Suggère des labels pour une note, filtrés par notebook */ async suggestLabels(noteContent: string): Promise { // 1. Récupérer tous les labels du notebook courant const existingLabels = await prisma.label.findMany({ where: { notebookId: this.notebookId } }) // 2. Utiliser le service existant pour générer des suggestions const allSuggestions = await autoTagService.suggestTags(noteContent) // 3. Filtrer: retourner seulement les labels qui existent dans ce notebook const contextualSuggestions = allSuggestions.filter(suggestion => existingLabels.some(label => label.name === suggestion) ) // 4. Si pas de correspondance, suggérer de créer un nouveau label (optionnel) if (contextualSuggestions.length === 0 && existingLabels.length < 50) { // Suggérer le meilleur candidat pour création return [allSuggestions[0]] // Premier = plus pertinent } return contextualSuggestions } /** * Crée automatiquement un label si le thème est récurrent (IA4) */ async createLabelIfRecurring(noteContent: string): Promise