feat(notes): liens internes, onglet Réseau, living blocks et consentement IA
Some checks failed
CI / Lint, Test & Build (push) Failing after 1m19s
CI / Deploy production (on server) (push) Has been skipped

Rend les liens entre notes visibles et persistants (sync NoteLink au save, auto-save, graphe réseau rafraîchi), ajoute living blocks, Memory Echo, recherche globale, consentement IA explicite et consolide les prototypes design en architectural-grid.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Antigravity
2026-05-24 14:27:29 +00:00
parent 077e665dfc
commit e2672cd2c2
323 changed files with 20670 additions and 42431 deletions

View File

@@ -5,6 +5,8 @@ import { useQueryClient } from '@tanstack/react-query'
import type { Notebook, Label, Note } from '@/lib/types'
import { LabelColorName, LABEL_COLORS } from '@/lib/types'
import { getHashColor } from '@/lib/utils'
import { emitNoteChange } from '@/lib/note-change-sync'
import { getNoteById } from '@/app/actions/notes'
import { useNoteRefresh } from './NoteRefreshContext'
import { toast } from 'sonner'
import { queryKeys } from '@/lib/query-keys'
@@ -104,7 +106,7 @@ export function NotebooksProvider({ children, initialNotebooks = [] }: Notebooks
const [isLoading, setIsLoading] = useState(true)
const [isMovingNote, setIsMovingNote] = useState(false)
const [error, setError] = useState<string | null>(null)
const { triggerRefresh, triggerNotebooksRefresh, notebooksRefreshKey } = useNoteRefresh()
const { triggerNotebooksRefresh, notebooksRefreshKey } = useNoteRefresh()
// ===== LABEL STATE (merged from LabelContext) =====
const [labels, setLabels] = useState<Label[]>([])
@@ -187,8 +189,7 @@ export function NotebooksProvider({ children, initialNotebooks = [] }: Notebooks
// Invalidate notebooks cache — React Query will re-fetch in bg
queryClient.invalidateQueries({ queryKey: queryKeys.notebooks() })
triggerNotebooksRefresh()
triggerRefresh()
}, [queryClient, triggerNotebooksRefresh, triggerRefresh])
}, [queryClient, triggerNotebooksRefresh])
const updateNotebook = useCallback(async (notebookId: string, data: UpdateNotebookInput) => {
const response = await fetch(`/api/notebooks/${notebookId}`, {
@@ -203,8 +204,7 @@ export function NotebooksProvider({ children, initialNotebooks = [] }: Notebooks
queryClient.invalidateQueries({ queryKey: queryKeys.notebooks() })
triggerNotebooksRefresh()
triggerRefresh()
}, [queryClient, triggerNotebooksRefresh, triggerRefresh])
}, [queryClient, triggerNotebooksRefresh])
const moveNotebookToParent = useCallback(async (notebookId: string, parentId: string | null) => {
const response = await fetch(`/api/notebooks/${notebookId}`, {
@@ -220,8 +220,7 @@ export function NotebooksProvider({ children, initialNotebooks = [] }: Notebooks
queryClient.invalidateQueries({ queryKey: queryKeys.notebooks() })
triggerNotebooksRefresh()
triggerRefresh()
}, [queryClient, triggerNotebooksRefresh, triggerRefresh])
}, [queryClient, triggerNotebooksRefresh])
const deleteNotebook = useCallback(async (notebookId: string) => {
const response = await fetch(`/api/notebooks/${notebookId}`, {
@@ -234,8 +233,7 @@ export function NotebooksProvider({ children, initialNotebooks = [] }: Notebooks
queryClient.invalidateQueries({ queryKey: queryKeys.notebooks() })
triggerNotebooksRefresh()
triggerRefresh()
}, [queryClient, triggerNotebooksRefresh, triggerRefresh])
}, [queryClient, triggerNotebooksRefresh])
const trashNotebook = useCallback(async (notebookId: string) => {
const response = await fetch(`/api/notebooks/${notebookId}`, {
@@ -250,8 +248,7 @@ export function NotebooksProvider({ children, initialNotebooks = [] }: Notebooks
queryClient.invalidateQueries({ queryKey: queryKeys.notebooks() })
triggerNotebooksRefresh()
triggerRefresh()
}, [queryClient, triggerNotebooksRefresh, triggerRefresh])
}, [queryClient, triggerNotebooksRefresh])
const restoreNotebook = useCallback(async (notebookId: string) => {
const response = await fetch(`/api/notebooks/${notebookId}`, {
@@ -266,8 +263,7 @@ export function NotebooksProvider({ children, initialNotebooks = [] }: Notebooks
queryClient.invalidateQueries({ queryKey: queryKeys.notebooks() })
triggerNotebooksRefresh()
triggerRefresh()
}, [queryClient, triggerNotebooksRefresh, triggerRefresh])
}, [queryClient, triggerNotebooksRefresh])
const permanentDeleteNotebook = useCallback(async (notebookId: string) => {
const response = await fetch(`/api/notebooks/${notebookId}`, {
@@ -280,8 +276,7 @@ export function NotebooksProvider({ children, initialNotebooks = [] }: Notebooks
queryClient.invalidateQueries({ queryKey: queryKeys.notebooks() })
triggerNotebooksRefresh()
triggerRefresh()
}, [queryClient, triggerNotebooksRefresh, triggerRefresh])
}, [queryClient, triggerNotebooksRefresh])
const updateNotebookOrderOptimistic = useCallback(async (notebookIds: string[]) => {
const response = await fetch('/api/notebooks/reorder', {
@@ -296,8 +291,7 @@ export function NotebooksProvider({ children, initialNotebooks = [] }: Notebooks
queryClient.invalidateQueries({ queryKey: queryKeys.notebooks() })
triggerNotebooksRefresh()
triggerRefresh()
}, [queryClient, triggerNotebooksRefresh, triggerRefresh])
}, [queryClient, triggerNotebooksRefresh])
// ===== LABEL ACTIONS (merged from LabelContext) =====
const addLabel = useCallback(async (name: string, color?: LabelColorName, labelNotebookId?: string | null) => {
@@ -315,13 +309,12 @@ export function NotebooksProvider({ children, initialNotebooks = [] }: Notebooks
setLabels(prev => [...prev, data.data])
// Invalidate labels cache
queryClient.invalidateQueries({ queryKey: queryKeys.labels(finalNotebookId) })
triggerRefresh()
}
} catch (error) {
console.error('Failed to add label:', error)
throw error
}
}, [notebookId, queryClient, triggerRefresh])
}, [notebookId, queryClient])
const getLabelColor = useCallback((name: string): LabelColorName => {
const label = labels.find(l => l.name.toLowerCase() === name.toLowerCase())
@@ -331,8 +324,7 @@ export function NotebooksProvider({ children, initialNotebooks = [] }: Notebooks
const refreshLabels = useCallback(async () => {
await fetchLabels(notebookId)
queryClient.invalidateQueries({ queryKey: queryKeys.labels(notebookId) })
triggerRefresh()
}, [fetchLabels, notebookId, queryClient, triggerRefresh])
}, [fetchLabels, notebookId, queryClient])
// ===== ACTIONS: LABELS (keep existing API-based ones for compatibility) =====
const createLabel = useCallback(async (data: CreateLabelInput) => {
@@ -349,9 +341,8 @@ export function NotebooksProvider({ children, initialNotebooks = [] }: Notebooks
const result = await response.json()
// Invalidate labels cache for this notebook
queryClient.invalidateQueries({ queryKey: queryKeys.labels(data.notebookId) })
triggerRefresh()
return result
}, [queryClient, triggerRefresh])
}, [queryClient])
const updateLabel = useCallback(async (labelId: string, data: UpdateLabelInput) => {
const response = await fetch(`/api/labels/${labelId}`, {
@@ -372,8 +363,7 @@ export function NotebooksProvider({ children, initialNotebooks = [] }: Notebooks
}
// Invalidate labels cache
queryClient.invalidateQueries({ queryKey: queryKeys.labels(notebookId) })
triggerRefresh()
}, [notebookId, queryClient, triggerRefresh])
}, [notebookId, queryClient])
const deleteLabel = useCallback(async (labelId: string) => {
const response = await fetch(`/api/labels/${labelId}`, {
@@ -387,8 +377,7 @@ export function NotebooksProvider({ children, initialNotebooks = [] }: Notebooks
setLabels(prev => prev.filter(label => label.id !== labelId))
// Invalidate labels cache
queryClient.invalidateQueries({ queryKey: queryKeys.labels(notebookId) })
triggerRefresh()
}, [notebookId, queryClient, triggerRefresh])
}, [notebookId, queryClient])
// ===== ACTIONS: NOTES =====
const moveNoteToNotebookOptimistic = useCallback(async (noteId: string, targetNotebookId: string | null) => {
@@ -404,18 +393,20 @@ export function NotebooksProvider({ children, initialNotebooks = [] }: Notebooks
throw new Error('Failed to move note')
}
// Invalidate notebooks and notes cache
queryClient.invalidateQueries({ queryKey: queryKeys.notebooks() })
queryClient.invalidateQueries({ queryKey: queryKeys.notes(null) }) // notes list
queryClient.invalidateQueries({ queryKey: queryKeys.notes(null) })
triggerNotebooksRefresh()
triggerRefresh()
const movedNote = await getNoteById(noteId)
if (movedNote) {
emitNoteChange({ type: 'updated', note: movedNote })
}
} catch (error) {
toast.error('Failed to move note. Please try again.')
throw error
} finally {
setIsMovingNote(false)
}
}, [queryClient, triggerRefresh, triggerNotebooksRefresh])
}, [queryClient, triggerNotebooksRefresh])
// ===== ACTIONS: AI (STUBS) =====
const suggestNotebookForNote = useCallback(async (_noteContent: string) => {

View File

@@ -0,0 +1,44 @@
'use client'
import { createContext, useContext, useState, useEffect, type ReactNode } from 'react'
import { SearchModal } from '@/components/search-modal'
interface SearchModalContextValue {
open: () => void
close: () => void
}
const SearchModalContext = createContext<SearchModalContextValue>({
open: () => {},
close: () => {},
})
export function useSearchModal() {
return useContext(SearchModalContext)
}
export function SearchModalProvider({ children }: { children: ReactNode }) {
const [isOpen, setIsOpen] = useState(false)
const open = () => setIsOpen(true)
const close = () => setIsOpen(false)
// Global keyboard shortcut: Ctrl+K or Cmd+K
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault()
setIsOpen(prev => !prev)
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [])
return (
<SearchModalContext.Provider value={{ open, close }}>
{children}
<SearchModal isOpen={isOpen} onClose={close} />
</SearchModalContext.Provider>
)
}