Keep/_bmad-output/planning-artifacts/notebooks-contextual-labels-architecture.md
sepehr 7fb486c9a4 feat: Complete internationalization and code cleanup
## Translation Files
- Add 11 new language files (es, de, pt, ru, zh, ja, ko, ar, hi, nl, pl)
- Add 100+ missing translation keys across all 15 languages
- New sections: notebook, pagination, ai.batchOrganization, ai.autoLabels
- Update nav section with workspace, quickAccess, myLibrary keys

## Component Updates
- Update 15+ components to use translation keys instead of hardcoded text
- Components: notebook dialogs, sidebar, header, note-input, ghost-tags, etc.
- Replace 80+ hardcoded English/French strings with t() calls
- Ensure consistent UI across all supported languages

## Code Quality
- Remove 77+ console.log statements from codebase
- Clean up API routes, components, hooks, and services
- Keep only essential error handling (no debugging logs)

## UI/UX Improvements
- Update Keep logo to yellow post-it style (from-yellow-400 to-amber-500)
- Change selection colors to #FEF3C6 (notebooks) and #EFB162 (nav items)
- Make "+" button permanently visible in notebooks section
- Fix grammar and syntax errors in multiple components

## Bug Fixes
- Fix JSON syntax errors in it.json, nl.json, pl.json, zh.json
- Fix syntax errors in notebook-suggestion-toast.tsx
- Fix syntax errors in use-auto-tagging.ts
- Fix syntax errors in paragraph-refactor.service.ts
- Fix duplicate "fusion" section in nl.json

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

Ou une version plus courte si vous préférez :

feat(i18n): Add 15 languages, remove logs, update UI components

- Create 11 new translation files (es, de, pt, ru, zh, ja, ko, ar, hi, nl, pl)
- Add 100+ translation keys: notebook, pagination, AI features
- Update 15+ components to use translations (80+ strings)
- Remove 77+ console.log statements from codebase
- Fix JSON syntax errors in 4 translation files
- Fix component syntax errors (toast, hooks, services)
- Update logo to yellow post-it style
- Change selection colors (#FEF3C6, #EFB162)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-11 22:26:13 +01:00

61 KiB

stepsCompleted inputDocuments workflowType project_name user_name date communication_language document_output_language focusArea
1
2
3
4
notebooks-contextual-labels-prd.md
notebooks-wireframes.md
project-context.md
architecture Keep - Notebooks & Labels Contextuels Ramez 2026-01-11 French English 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:

// 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

// 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:

// 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:

// 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<void>
  updateNotebookOrderOptimistic: (notebookIds: string[]) => Promise<void>
  moveNoteToNotebookOptimistic: (noteId: string, notebookId: string | null) => Promise<void>

  // Actions IA (non-optimistes, asynchrones)
  suggestNotebookForNote: (noteContent: string) => Promise<Notebook | null>
  suggestLabelsForNote: (noteContent: string, notebookId: string) => Promise<Label[]>
}

export const NotebooksContext = createContext<NotebooksContextValue | null>(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<Notebook[]>([])
  const [currentNotebook, setCurrentNotebook] = useState<Notebook | null>(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 (
    <NotebooksContext.Provider value={{
      notebooks: optimisticNotebooks,
      currentNotebook,
      currentLabels,
      createNotebookOptimistic,
      // ... autres actions
    }}>
      {children}
    </NotebooksContext.Provider>
  )
}

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:

// 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<string[]> {
    // 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<Label | null> {
    const suggestions = await autoTagService.suggestTags(noteContent)
    const topSuggestion = suggestions[0]

    // Vérifier si ce label existe déjà
    const existing = await prisma.label.findFirst({
      where: {
        notebookId: this.notebookId,
        name: topSuggestion
      }
    })

    if (existing) return existing // Déjà existe

    // Créer le nouveau label
    return await prisma.label.create({
      data: {
        name: topSuggestion,
        notebookId: this.notebookId,
        color: getRandomColor()
      }
    })
  }
}

// app/actions/ai.ts (Server Actions)
export async function suggestContextualLabels(noteId: string, notebookId: string) {
  const session = await auth()
  if (!session?.user?.id) throw new Error('Unauthorized')

  const note = await prisma.note.findUnique({
    where: { id: noteId, userId: session.user.id }
  })

  if (!note) throw new Error('Note not found')

  const service = new ContextualAutoTagService(notebookId)
  const suggestions = await service.suggestLabels(note.content)

  return suggestions
}

export async function suggestNotebookForNote(noteId: string) {
  const session = await auth()
  if (!session?.user?.id) throw new Error('Unauthorized')

  const note = await prisma.note.findUnique({
    where: { id: noteId, userId: session.user.id },
    include: {
      notebook: true
    }
  })

  if (!note || note.notebook) {
    // Déjà dans un notebook, pas de suggestion
    return null
  }

  // Récupérer tous les notebooks de l'utilisateur
  const notebooks = await prisma.notebook.findMany({
    where: { userId: session.user.id },
    include: { labels: true }
  })

  // Utiliser l'IA pour suggérer le meilleur notebook
  const prompt = `
    Given this note content and the list of available notebooks,
    suggest the most appropriate notebook.

    Note: ${note.content}

    Available notebooks:
    ${notebooks.map(nb => `- ${nb.name}: ${nb.labels.map(l => l.name).join(', ')}`).join('\n')}

    Return ONLY the notebook name, or "NONE" if no good match.
  `

  const response = await generateText({
    model: getOpenAIModel(),
    prompt
  })

  const suggestedName = response.text.trim()
  const suggestedNotebook = notebooks.find(nb => nb.name === suggestedName)

  return suggestedNotebook
}

Mapping des 6 Features IA:

Feature IA Service Existant Adaptation Contextuelle
IA1: Suggestion Notebook N/A (nouveau) suggestNotebookForNote() - analyse contenu + notebooks existants
IA2: Suggestion Labels autoTagService ContextualAutoTagService - filtre par notebookId
IA3: Organisation Batch autoTagService organizeInboxBatch() - déplace notes + suggère notebooks
IA4: Création Auto Labels autoTagService createLabelIfRecurring() - détect thèmes récurrents
IA5: Recherche Sémantique semanticSearchService Filtrer query Prisma par notebookId
IA6: Synthèse Notebook N/A (nouveau) generateNotebookSummary() - résume toutes les notes d'un notebook

Justification:

  1. Réutilisation maximum - Services existants non modifiés
  2. Pattern Adapter - Couche d'adaptation légère pour le contexte
  3. Filtrage DB-level - Performance optimale (pas de post-traitement)
  4. Testabilité - Services isolés, faciles à tester unitairement

🔑 DECISION ARCHITECTURALE #5: Drag & Drop Implementation Details

Question: Comment implémenter concrètement le drag & drop de notebooks dans la sidebar avec Muuri ?

Contraintes:

  • Utiliser Muuri (pas de @dnd-kit)
  • Même configuration que masonry-grid pour consistance
  • Performance < 100ms latence perçue
  • Optimistic UI avec synchronisation serveur

DÉCISION RETENUE: Instance Muuri dédiée pour la sidebar

Architecture:

// components/notebooks-sidebar.tsx
'use client'

import { useEffect, useRef, useCallback } from 'react'
import { useNotebooks } from '@/app/context/notebooks-context'

export function NotebooksSidebar() {
  const { notebooks, currentNotebook, setCurrentNotebook, updateNotebookOrderOptimistic } = useNotebooks()
  const notebooksGridRef = useRef<HTMLDivElement>(null)
  const notebooksMuuri = useRef<any>(null)

  const handleDragEnd = useCallback(async (grid: any) => {
    if (!grid) return

    const items = grid.getItems()
    const notebookIds = items
      .map((item: any) => item.getElement()?.getAttribute('data-id'))
      .filter((id: any): id is string => !!id)

    // Optimistic update via useOptimistic
    await updateNotebookOrderOptimistic(notebookIds)
  }, [updateNotebookOrderOptimistic])

  useEffect(() => {
    let isMounted = true

    const initNotebooksMuuri = async () => {
      // Import Muuri dynamiquement
      const MuuriClass = (await import('muuri')).default

      if (!isMounted || !notebooksGridRef.current) return

      // Configuration IDENTIQUE à masonry-grid pour consistance
      const layoutOptions = {
        dragEnabled: true,
        dragContainer: document.body,
        dragStartPredicate: {
          distance: 5, // Plus sensible que la masonry (sidebar plus compacte)
          delay: 0,
        },
        dragPlaceholder: {
          enabled: true,
          createElement: (item: any) => {
            const el = item.getElement().cloneNode(true)
            el.style.opacity = '0.5'
            return el
          },
        },
        // Layout vertical pour sidebar
        layout: {
          fillGaps: false,
          horizontal: false,
          alignRight: false,
          alignBottom: false,
          rounding: false,
        }
      }

      notebooksMuuri.current = new MuuriClass(notebooksGridRef.current, layoutOptions)
        .on('dragEnd', () => handleDragEnd(notebooksMuuri.current))
    }

    initNotebooksMuuri()

    return () => {
      isMounted = false
      notebooksMuuri.current?.destroy()
    }
  }, [handleDragEnd])

  // Synchroniser quand les notebooks changent
  useEffect(() => {
    if (notebooksMuuri.current) {
      notebooksMuuri.current.refreshItems().layout()
    }
  }, [notebooks])

  return (
    <div className="notebooks-sidebar w-64 bg-gray-50 p-4">
      <h2 className="text-sm font-semibold mb-4">Notebooks</h2>

      <div ref={notebooksGridRef} className="notebooks-list">
        {/* "Notes générales" - toujours en premier */}
        <div
          className={`notebook-item p-3 rounded cursor-pointer hover:bg-gray-200 ${
            !currentNotebook ? 'bg-blue-100' : ''
          }`}
          onClick={() => setCurrentNotebook(null)}
          data-id="general-notes"
        >
          📝 Notes générales
        </div>

        {/* Liste des notebooks Muuri */}
        {notebooks.map(notebook => (
          <div
            key={notebook.id}
            className={`notebook-item p-3 rounded cursor-pointer hover:bg-gray-200 ${
              currentNotebook?.id === notebook.id ? 'bg-blue-100' : ''
            }`}
            onClick={() => setCurrentNotebook(notebook)}
            data-id={notebook.id}
          >
            {notebook.icon || '📚'} {notebook.name}
            <span className="ml-auto text-xs text-gray-500">
              {notebook.notes?.length || 0} notes
            </span>
          </div>
        ))}
      </div>
    </div>
  )
}

Drag & Drop Notes vers Notebooks:

// Extension de masonry-grid.tsx
// Ajouter des zones de drop pour chaque notebook

const handleNoteDragEnd = useCallback(async (grid: any) => {
  if (!grid) return

  const items = grid.getItems()
  const noteIds = items
    .map((item: any) => item.getElement()?.getAttribute('data-id'))
    .filter((id: any): id is string => !!id)

  // Update order
  await updateFullOrderWithoutRevalidation(noteIds)

  // CHANGEMENT: Détecter si note déposée sur un notebook
  const draggedOverNotebook = detectNotebookDropTarget()

  if (draggedOverNotebook) {
    await moveNotesToNotebook(noteIds, draggedOverNotebook.id)
  }
}, [])

function detectNotebookDropTarget(): Notebook | null {
  // Détecter quel élément de sidebar est sous la souris
  const sidebarElement = document.elementFromPoint(mouseX, mouseY)
  const notebookItem = sidebarElement?.closest('.notebook-item')

  if (!notebookItem) return null

  const notebookId = notebookItem.getAttribute('data-id')
  if (notebookId === 'general-notes') return null // Inbox

  return notebooks.find(nb => nb.id === notebookId) || null
}

Justification:

  1. Configuration partagée - Options drag & drop synchronisées avec masonry-grid
  2. Performance - Muuri optimisé pour sidebar (layout vertical simple)
  3. UX cohérente - Même comportement de drag dans toute l'app
  4. Optimistic UI - useOptimistic pour réactivité immédiate

🔑 DECISION ARCHITECTURALE #6: Migration Strategy (Brownfield)

Question: Comment migrer du système de tags plat vers notebooks sans breaking changes ?

Contraintes:

  • ZERO breaking changes - l'ancien système doit continuer de fonctionner
  • Données existantes préservées
  • Migration transparente pour l'utilisateur
  • Rollback possible si nécessaire

DÉCISION RETENUE: Migration Graduelle avec Compatibilité Ascendante

Plan de Migration en 4 Phases:

// scripts/migrate-to-notebooks.ts

/**
 * PHASE 1: Préparation du Schema (Non-Breaking)
 * - Ajouter les tables Notebook et Label
 * - Ajouter notebookId optionnel sur Note (default: null)
 * - Laisser l'ancien système de tags intact
 */
async function phase1_prepareSchema() {
  // Exécuter Prisma migrate
  await prisma.$executeRaw`
    CREATE TABLE "Notebook" (
      id TEXT PRIMARY KEY,
      name TEXT NOT NULL,
      icon TEXT,
      color TEXT,
      "order" INTEGER NOT NULL,
      "userId" TEXT NOT NULL,
      "createdAt" DATETIME DEFAULT CURRENT_TIMESTAMP,
      "updatedAt" DATETIME DEFAULT CURRENT_TIMESTAMP,
      FOREIGN KEY ("userId") REFERENCES "User"(id) ON DELETE CASCADE
    );

    CREATE TABLE "Label" (
      id TEXT PRIMARY KEY,
      name TEXT NOT NULL,
      color TEXT,
      "notebookId" TEXT NOT NULL,
      "createdAt" DATETIME DEFAULT CURRENT_TIMESTAMP,
      "updatedAt" DATETIME DEFAULT CURRENT_TIMESTAMP,
      FOREIGN KEY ("notebookId") REFERENCES "Notebook"(id) ON DELETE CASCADE,
      UNIQUE("notebookId", "name")
    );

    ALTER TABLE "Note" ADD COLUMN "notebookId" TEXT;
    ALTER TABLE "Note" ADD FOREIGN KEY ("notebookId") REFERENCES "Notebook"(id) ON DELETE SETNULL;
    CREATE INDEX "Note_notebookId_idx" ON "Note"("userId", "notebookId");
  `

  console.log('✅ Phase 1: Schema préparé (compatible avec ancien système)')
}

/**
 * PHASE 2: Migration des Données (Transparent)
 * - Créer un notebook par défaut
 * - Déplacer tous les labels existants vers ce notebook
 * - Notes restent sans notebook (Inbox)
 */
async function phase2_migrateData(userId: string) {
  // 2.1 Créer notebook de migration
  const migrationNotebook = await prisma.notebook.create({
    data: {
      name: "Labels Migrés",
      icon: "📦",
      order: 999,
      userId
    }
  })

  // 2.2 Attribuer tous les labels existants à ce notebook
  const existingLabels = await prisma.label.findMany({
    where: { userId } // Assuming labels have userId
  })

  await Promise.all(
    existingLabels.map(label =>
      prisma.label.update({
        where: { id: label.id },
        data: { notebookId: migrationNotebook.id }
      })
    )
  )

  // 2.3 Laisser toutes les notes sans notebook (Inbox par défaut)
  // Les notes resteront dans "Notes générales"

  console.log('✅ Phase 2: Données migrées (labels préservés)')
}

/**
 * PHASE 3: Activation de la Nouvelle UI (Opt-in)
 * - Ajouter la sidebar notebooks
 * - Permettre aux utilisateurs de créer de nouveaux notebooks
 * - L'ancien système de tags continue de fonctionner via le notebook de migration
 */
async function phase3_enableNewUI() {
  // Déployer la nouvelle UI avec sidebar
  // Les utilisateurs peuvent commencer à utiliser les notebooks
  // L'ancien système reste accessible via "Labels Migrés"

  console.log('✅ Phase 3: Nouvelle UI déployée (opt-in)')
}

/**
 * PHASE 4: Nettoyage (Futur - Optionnel)
 * - Après adoption massive, supprimer l'ancien système
 * - SEULEMENT après confirmation que tous les utilisateurs ont migré
 */
async function phase4_cleanup() {
  // À exécuter manuellement après 6-12 mois
  // Supprimer l'ancien système de tags global

  console.log('⚠️  Phase 4: Nettoyage (futur, optionnel)')
}

Compatibility Layer (Pendant la transition):

// lib/compatibility/legacy-tags.ts

/**
 * Permet à l'ancien code de continuer à fonctionner
 * pendant la période de transition
 */
export class LegacyTagsCompatibility {
  /**
   * Retourne tous les labels (mode compatibilité)
   * - Si notebookId spécifié: labels de ce notebook
   * - Sinon: tous les labels (ancien comportement)
   */
  static async getLabels(userId: string, notebookId?: string) {
    if (notebookId) {
      // Nouveau système: labels contextuels
      return await prisma.label.findMany({
        where: { notebook: { userId, id: notebookId } }
      })
    } else {
      // Ancien système: tous les labels (backward compat)
      return await prisma.label.findMany({
        where: { notebook: { userId } }
      })
    }
  }

  /**
   * Ajoute un label à une note (mode compatibilité)
   * - Crée automatiquement le notebook de migration si nécessaire
   */
  static async addLabelToNote(noteId: string, labelName: string) {
    const note = await prisma.note.findUnique({ where: { id: noteId } })

    if (!note?.notebookId) {
      // Note sans notebook: utiliser le notebook de migration
      const migrationNotebook = await this.getOrCreateMigrationNotebook(note.userId)
      return await this.attachLabel(note, labelName, migrationNotebook.id)
    } else {
      // Note avec notebook: attacher au notebook existant
      return await this.attachLabel(note, labelName, note.notebookId)
    }
  }

  private static async getOrCreateMigrationNotebook(userId: string) {
    let notebook = await prisma.notebook.findFirst({
      where: { name: "Labels Migrés", userId }
    })

    if (!notebook) {
      notebook = await prisma.notebook.create({
        data: { name: "Labels Migrés", userId, order: 999 }
      })
    }

    return notebook
  }

  private static async attachLabel(note: Note, labelName: string, notebookId: string) {
    // Créer le label s'il n'existe pas
    let label = await prisma.label.findFirst({
      where: { notebookId, name: labelName }
    })

    if (!label) {
      label = await prisma.label.create({
        data: { name: labelName, notebookId }
      })
    }

    // Attacher à la note
    return await prisma.note.update({
      where: { id: note.id },
      data: { labels: { connect: { id: label.id } } }
    })
  }
}

Justification:

  1. Non-breaking - L'ancien système continue de fonctionner
  2. Transparent - L'utilisateur ne perd aucune donnée
  3. Progressif - Adoption en douceur, pas de "big bang"
  4. Rollback-friendly - Chaque phase peut être annulée
  5. Future-proof - Prépare le terrain pour supprimer l'ancien système plus tard

🔑 DECISION ARCHITECTURALE #7: Search Contextualization

Question: Comment limiter la recherche sémantique au notebook actuel ?

Contraintes:

  • Réutiliser le service de recherche existant
  • Filtrer au niveau DB (pas post-traitement)
  • Performance < 300ms
  • Supporter "Notes générales" (toutes les notes sans notebook)

DÉCISION RETENUE: Filtrage Prisma avec requête conditionnelle

Architecture:

// app/actions/semantic-search.ts

export async function contextualSemanticSearch(query: string, notebookId: string | null) {
  const session = await auth()
  if (!session?.user?.id) throw new Error('Unauthorized')

  // Générer l'embedding pour la requête
  const queryEmbedding = await generateEmbedding(query)

  // CONSTRUCTION CONDITIONNELLE DE LA REQUÊTE
  const baseWhere = {
    userId: session.user.id,
  }

  // FILTRAGE PAR NOTEBOOK
  const whereClause = notebookId
    ? { ...baseWhere, notebookId } // Notebook spécifique
    : { ...baseWhere, notebookId: null } // "Notes générales" uniquement

  // Si on veut rechercher dans TOUT (Inbox + tous les notebooks)
  // const whereClause = baseWhere // Pas de filtre notebook

  // REQUÊTE PRISMA AVEC FILTRAGE
  const results = await prisma.$queryRaw`
    SELECT
      n.*,
      v.embedding <#> ${queryEmbedding} AS distance
    FROM "Note" n
    LEFT JOIN "_NoteToLabel" ntl ON n.id = ntl."A"
    LEFT JOIN "Label" l ON ntl."B" = l.id
    WHERE n."userId" = ${session.user.id}
      AND ${notebookId ? Prisma.sql`n."notebookId" = ${notebookId}` : Prisma.sql`n."notebookId" IS NULL`}
    ORDER BY distance ASC
    LIMIT 20
  `

  // Retourner les résultats
  return results as Note[]
}

UI Integration:

// components/search-bar.tsx
'use client'

import { useState } from 'react'
import { useNotebooks } from '@/app/context/notebooks-context'

export function SearchBar() {
  const { currentNotebook } = useNotebooks()
  const [query, setQuery] = useState('')
  const [results, setResults] = useState<Note[]>([])

  const handleSearch = async (searchQuery: string) => {
    setQuery(searchQuery)

    if (searchQuery.length < 2) {
      setResults([])
      return
    }

    // RECHERCHE CONTEXTUELLE
    const searchResults = await contextualSemanticSearch(
      searchQuery,
      currentNotebook?.id || null // null = Notes générales
    )

    setResults(searchResults)
  }

  return (
    <div className="search-bar">
      <input
        type="text"
        placeholder={currentNotebook
          ? `Rechercher dans "${currentNotebook.name}"`
          : "Rechercher dans les Notes générales"
        }
        value={query}
        onChange={(e) => handleSearch(e.target.value)}
      />

      {results.length > 0 && (
        <div className="search-results">
          <p className="text-xs text-gray-500">
            {results.length} résultat{results.length > 1 ? 's' : ''} dans{' '}
            {currentNotebook ? currentNotebook.name : 'Notes générales'}
          </p>
          {results.map(note => (
            <SearchResultItem key={note.id} note={note} />
          ))}
        </div>
      )}
    </div>
  )
}

Variante: Recherche Globale (Option Future):

// Si on veut permettre la recherche dans TOUS les notebooks
export async function globalSemanticSearch(query: string) {
  const session = await auth()
  if (!session?.user?.id) throw new Error('Unauthorized')

  const queryEmbedding = await generateEmbedding(query)

  // PAS DE FILTRE NOTEBOOK
  const results = await prisma.$queryRaw`
    SELECT
      n.*,
      nb.name AS "notebookName",
      v.embedding <#> ${queryEmbedding} AS distance
    FROM "Note" n
    LEFT JOIN "Notebook" nb ON n."notebookId" = nb.id
    WHERE n."userId" = ${session.user.id}
    ORDER BY distance ASC
    LIMIT 20
  `

  return results as (Note & { notebookName: string | null })[]
}

Justification:

  1. Performance - Filtrage au niveau DB (pas de post-traitement JavaScript)
  2. Simplicité - Réutilise le service existant avec une clause where supplémentaire
  3. Flexibilité - Facile à étendre pour "recherche globale" plus tard
  4. UX claire - L'utilisateur sait où il recherche (notebook actuel)

🔑 DECISION ARCHITECTURALE #8: Undo/Redo System

Question: Comment permettre l'annulation d'actions destructives (déplacements, suppressions) ?

Contraintes:

  • UX sans peur (l'utilisateur ose explorer)
  • Actions concernées: déplacement notes, suppression notebooks, suppression labels
  • Pas d'historique infini (limite à 20 actions)
  • Implementation simple

DÉCISION RETENUE: Historique d'Actions Simples avec Server Actions Rollback

Architecture:

// lib/undo-history.ts

interface Action {
  type: 'MOVE_NOTE' | 'DELETE_NOTEBOOK' | 'DELETE_LABEL' | 'REORDER_NOTEBOOKS'
  timestamp: Date
  undo: () => Promise<void>
  description: string
}

class UndoHistory {
  private history: Action[] = []
  private maxHistory = 20

  add(action: Action) {
    this.history.push(action)
    if (this.history.length > this.maxHistory) {
      this.history.shift() // Supprimer l'action la plus ancienne
    }
  }

  async undo() {
    const lastAction = this.history.pop()
    if (!lastAction) return

    await lastAction.undo()
  }

  getLastAction(): Action | null {
    return this.history[this.history.length - 1] || null
  }
}

export const undoHistory = new UndoHistory()

// app/actions/notes.ts
export async function moveNoteToNotebook(noteId: string, notebookId: string | null) {
  const session = await auth()
  if (!session?.user?.id) throw new Error('Unauthorized')

  const note = await prisma.note.findUnique({
    where: { id: noteId, userId: session.user.id }
  })

  if (!note) throw new Error('Note not found')

  const previousNotebookId = note.notebookId

  // Exécuter l'action
  const updatedNote = await prisma.note.update({
    where: { id: noteId },
    data: { notebookId }
  })

  // Enregistrer dans l'historique
  undoHistory.add({
    type: 'MOVE_NOTE',
    timestamp: new Date(),
    description: `Déplacer "${note.title || 'Sans titre'}" vers ${notebookId ? 'notebook' : 'Notes générales'}`,
    undo: async () => {
      // Rollback: remettre dans l'ancien notebook
      await prisma.note.update({
        where: { id: noteId },
        data: { notebookId: previousNotebookId }
      })
      revalidatePath('/')
    }
  })

  revalidatePath('/')
  return updatedNote
}

export async function deleteNotebook(notebookId: string) {
  const session = await auth()
  if (!session?.user?.id) throw new Error('Unauthorized')

  const notebook = await prisma.notebook.findUnique({
    where: { id: notebookId, userId: session.user.id },
    include: { notes: true }
  })

  if (!notebook) throw new Error('Notebook not found')

  // Sauvegarder les données pour rollback
  const notebookData = JSON.stringify(notebook)

  // Supprimer le notebook (CASCADE delete les labels)
  await prisma.notebook.delete({
    where: { id: notebookId }
  })

  // Les notes deviennent "générales" (onDelete: SetNull)

  // Enregistrer dans l'historique
  undoHistory.add({
    type: 'DELETE_NOTEBOOK',
    timestamp: new Date(),
    description: `Supprimer le notebook "${notebook.name}"`,
    undo: async () => {
      // Rollback: recréer le notebook
      const data = JSON.parse(notebookData)
      await prisma.notebook.create({
        data: {
          id: data.id,
          name: data.name,
          icon: data.icon,
          color: data.color,
          order: data.order,
          userId: data.userId
        }
      })

      // Recréer les labels
      for (const label of data.labels) {
        await prisma.label.create({
          data: {
            id: label.id,
            name: label.name,
            color: label.color,
            notebookId: data.id
          }
        })
      }

      revalidatePath('/')
    }
  })

  revalidatePath('/')
}

// app/actions/undo.ts
export async function undoLastAction() {
  const session = await auth()
  if (!session?.user?.id) throw new Error('Unauthorized')

  await undoHistory.undo()

  revalidatePath('/')
}

UI Integration:

// components/undo-toast.tsx
'use client'

import { useEffect, useState } from 'react'
import { undoLastAction } from '@/app/actions/undo'

export function UndoToast() {
  const [lastAction, setLastAction] = useState<string | null>(null)

  useEffect(() => {
    // Poll pour détecter nouvelles actions
    const interval = setInterval(() => {
      // Récupérer la dernière action depuis le serveur
      // (simplifié - en pratique on utiliserait un hook ou un contexte)
    }, 1000)

    return () => clearInterval(interval)
  }, [])

  if (!lastAction) return null

  return (
    <div className="fixed bottom-4 right-4 bg-gray-800 text-white px-4 py-2 rounded shadow-lg">
      <span className="mr-2">{lastAction}</span>
      <button
        onClick={() => undoLastAction()}
        className="underline hover:no-underline"
      >
        Annuler
      </button>
    </div>
  )
}

Justification:

  1. Simplicité - Pas de bibliothèque complexe (pas d'immer, redux-undo, etc.)
  2. Performance - Historique limité à 20 actions
  3. UX claire - Toast avec bouton "Annuler" (comme Gmail)
  4. Suffisant - Couvre les cas d'usage principaux (move, delete)

Architecture Summary

Decision Matrix

Voici un résumé de toutes les décisions architecturales prises :

# Decision Technology / Pattern Key Benefit Risk Mitigation
1 Drag & Drop Unifié Muuri (2 instances) UX cohérente, une seule library à maîtriser Performance validée en production
2 Database Schema Prisma extensible avec relations optionnelles ZERO breaking changes, migration douce Cascade delete maîtrisé (SetNull)
3 State Management React Context + useOptimistic + Server Actions État distribué gérable, optimistic UI natif React 19 Couches séparées pour clarté
4 IA Contextuelle Pattern Adapter avec services existants Réutilisation maximum, pas de duplication Filtrage DB-level pour performance
5 Drag & Drop Details Instance Muuri dédiée pour sidebar Configuration partagée avec masonry-grid Sync via useOptimistic
6 Migration Strategy 4 phases avec compatibilité ascendante Non-breaking, rollback possible Progressif, pas de "big bang"
7 Search Contextualization Filtrage Prisma conditionnel Performance DB-level, pas post-traitement Extension facile pour recherche globale
8 Undo/Redo System Historique simple avec rollback UX sans peur, implementation légère Limité à 20 actions pour performance

Technology Stack Final

Frontend:
  Framework: Next.js 16.1.1 (App Router)
  UI: React 19.2.3 (Strict Mode)
  State: React Context + useOptimistic (React 19 natif)
  Drag & Drop: Muuri (déjà en place)
  Icons: lucide-react (optionnel, déjà présent)

Backend:
  API: Server Actions (pattern existant)
  ORM: Prisma 5.22.0
  Database: SQLite

AI:
  SDK: Vercel AI SDK 6.0.23 (déjà en place)
  Providers: OpenAI / Ollama (déjà configurés)
  Services: Adapter layer pour contextualisation

NOUVELLES DÉPENDANCES: AUCUNE (0)

Component Architecture Map

keep-notes/
├── app/
│   ├── context/
│   │   └── notebooks-context.tsx          # 🆕 État global notebooks
│   ├── actions/
│   │   ├── notebooks.ts                   # 🆕 CRUD notebooks
│   │   ├── labels.ts                      # 🆕 CRUD labels contextuels
│   │   ├── ai.ts                          # 🔄 Adapter IA contextuelle
│   │   ├── undo.ts                        # 🆕 Système d'annulation
│   │   └── notes.ts                       # 🔄 Extension pour notebooks
│   ├── components/
│   │   ├── notebooks-sidebar.tsx          # 🆕 Sidebar avec drag & drop
│   │   ├── notebook-item.tsx              # 🆕 Item notebook individuel
│   │   ├── label-chip.tsx                 # 🆕 Chip label contextuel
│   │   ├── masonry-grid.tsx               # 🔄 Extension pour drop notebooks
│   │   └── undo-toast.tsx                 # 🆕 Toast d'annulation
│   └── providers.tsx                      # 🔄 Ajout NotebooksProvider
│
├── lib/
│   ├── ai/
│   │   └── services/
│   │       ├── contextual-auto-tag.service.ts    # 🆕 IA contextuelle
│   │       ├── notebook-suggestion.service.ts    # 🆕 IA1
│   │       └── notebook-summary.service.ts       # 🆕 IA6
│   ├── compatibility/
│   │   └── legacy-tags.ts                # 🆕 Couche compatibilité
│   └── undo-history.ts                   # 🆕 Historique d'actions
│
├── prisma/
│   └── schema.prisma                     # 🔄 Ajout Notebook + Label
│
└── scripts/
    └── migrate-to-notebooks.ts            # 🆕 Migration 4 phases

Data Flow Diagram

┌────────────────────────────────────────────────────────────────┐
│                        USER INTERACTION                        │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐        │
│  │ Drag & Drop  │  │  Create      │  │  Search      │        │
│  │  Notes       │  │  Notebook    │  │  Contextual  │        │
│  └──────┬───────┘  └──────┬───────┘  └──────┬───────┘        │
└─────────┼──────────────────┼──────────────────┼────────────────┘
          │                  │                  │
          ▼                  ▼                  ▼
┌────────────────────────────────────────────────────────────────┐
│  LAYER 1: OPTIMISTIC UI (useOptimistic)                       │
│  - Mise à jour immédiate de l'UI                              │
│  - Feedback visuel instantané                                 │
└────────────────────────────────────────────────────────────────┘
          │                  │                  │
          ▼                  ▼                  ▼
┌────────────────────────────────────────────────────────────────┐
│  LAYER 2: SERVER ACTIONS (app/actions/)                       │
│  - Validation auth/permissions                                │
│  - Business logic                                             │
│  - Undo history registration                                  │
└────────────────────────────────────────────────────────────────┘
          │                  │                  │
          ▼                  ▼                  ▼
┌────────────────────────────────────────────────────────────────┐
│  LAYER 3: DATABASE (Prisma + SQLite)                          │
│  - Notebook/Label CRUD                                         │
│  - Relations & Cascade delete                                 │
│  - Indexes pour performance                                   │
└────────────────────────────────────────────────────────────────┘
          │                  │                  │
          ▼                  ▼                  ▼
┌────────────────────────────────────────────────────────────────┐
│  LAYER 4: AI SERVICES (Contextual)                            │
│  - Notebook suggestions (IA1)                                 │
│  - Label suggestions (IA2)                                    │
│  - Semantic search limitée (IA5)                              │
└────────────────────────────────────────────────────────────────┘
          │                  │                  │
          ▼                  ▼                  ▼
┌────────────────────────────────────────────────────────────────┐
│  LAYER 5: CACHE INVALIDATION                                  │
│  - revalidatePath('/')                                         │
│  - Refresh React Context                                      │
└────────────────────────────────────────────────────────────────┘

Performance Targets vs Implementation

Requirement Target Implementation Confidence
Drag & drop latence < 100ms Muuri + useOptimistic (sans revalidatePath) Validé en production
Recherche sémantique < 300ms Filtrage Prisma DB-level Basé sur existant
Suggestions IA < 2s Vercel AI SDK (déjà en place) Validé en production
Sidebar chargement < 200ms React Context + useEffect Standard React
Migration rollback < 5s Script Prisma + transaction Basé sur DB size

Risk Assessment

Risk Impact Probability Mitigation
Performance dégradée avec 1000+ notes HIGH MEDIUM Indexes Prisma, pagination, lazy loading
Drag & drop bug entre sidebar et grid MEDIUM LOW Tests E2E, détection collision, fallback menu
IA suggestions incohérentes MEDIUM MEDIUM Validation labels existants, feedback user
Migration data loss CRITICAL VERY LOW Backup avant migration, rollback, tests staging
État global désynchronisé MEDIUM MEDIUM React Context unique source of truth, revalidation

Implementation Roadmap

Phase 1: Foundation (Week 1-2)

Objectif: Préparer le terrain sans toucher à l'existant

  1. Database Schema

    • Créer migration Prisma (Notebook, Label modifiés)
    • Tester localement avec prisma migrate dev
    • Valider indexes et cascade delete
  2. State Management

    • Créer NotebooksContext et NotebooksProvider
    • Intégrer dans app/providers.tsx
    • Créer hooks useNotebooks()
  3. Base Server Actions

    • createNotebook(), updateNotebook(), deleteNotebook()
    • createLabel(), updateLabel(), deleteLabel()
    • moveNoteToNotebook()

Phase 2: Core UI (Week 2-3)

Objectif: Implémenter la sidebar et navigation

  1. Notebooks Sidebar

    • Composant NotebooksSidebar avec Muuri
    • Drag & drop réorganisation notebooks
    • "Notes générales" item
  2. Navigation

    • Filtrer notes par notebook (Inbox vs Notebook)
    • Mettre à jour masonry-grid pour afficher notes filtrées
    • Breadcrumb/indicateur de notebook actuel
  3. CRUD UI

    • Modal création notebook
    • Modal création label
    • Menu contextuel (clic droit) sur notebooks

Phase 3: Drag & Drop Advanced (Week 3-4)

Objectif: Déplacer notes entre notebooks

  1. Cross-container Drag & Drop

    • Détecter drop sur sidebar
    • Déplacer note vers notebook
    • Déplacer note vers "Notes générales"
  2. Menu Contextuel Alternative

    • Clic droit → "Déplacer vers..."
    • Liste des notebooks disponibles
    • Fallback pour utilisateurs sans drag & drop

Phase 4: IA Integration (Week 4-5)

Objectif: Adapter l'IA existante pour le contexte

  1. IA Services

    • ContextualAutoTagService
    • NotebookSuggestionService
    • NotebookSummaryService
  2. UI IA Features

    • IA1: Suggestion notebook quand note créée dans Inbox
    • IA2: Suggestion labels contextuels
    • IA5: Recherche sémantique limitée au notebook

Phase 5: Polish & Testing (Week 5-6)

Objectif: Finaliser et tester

  1. Undo/Redo

    • Implémenter UndoHistory
    • Toast d'annulation
    • Tester tous les cas de rollback
  2. Migration

    • Script migration 4 phases
    • Tests staging avec données réelles
    • Documentation migration
  3. Testing

    • Tests E2E drag & drop
    • Tests performance (1000+ notes)
    • Tests IA suggestions

Conclusion

Architectural Principles Applied

  1. Brownfield-First - Réutilisation maximale de l'existant, ZERO breaking changes
  2. Single Responsibility - Chaque composant/service a une responsabilité claire
  3. Performance First - Optimisations DB-level, optimistic UI pour UX fluide
  4. Progressive Enhancement - Migration en 4 phases, adoption graduelle
  5. Simplicity over Complexity - Pas de Redux/Zustand/dnd-kit, utiliser le natif React

Key Success Factors

Muuri pour tout drag & drop - Un seul library, UX cohérente React Context + useOptimistic - État gérable sans librairie externe Pattern Adapter pour IA - Réutilisation du code existant Migration graduelle - Pas de "big bang", rollback possible Zéro nouvelle dépendance majeure - 100% technologies existantes

Next Steps

  1. Valider ce document - Obtenir approbation architecturale
  2. Créer tech specs détaillées - Pour chaque composant majeur
  3. Lancer Phase 1 - Database schema + state management
  4. Itérer rapidement - 2 semaines par phase, validation continue

Document Status: COMPLETE & VALIDATED

Validation Date: 2026-01-11 Validated By: Ramez (Product Owner) Architect: Winston (AI Agent)

Total Architectural Decisions: 8 Total Components: ~10-12 New Dependencies: 0 Breaking Changes: 0 Estimated Implementation Time: 5-6 weeks


This architecture document was created collaboratively using the BMAD Architecture Workflow. Date: 2026-01-11 Architect: Winston (AI Agent) Project: Keep - Notebooks & Labels Contextuels