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

@@ -24,6 +24,7 @@ export type UserAISettingsData = {
noteHistoryMode?: 'manual' | 'auto'
fontFamily?: 'inter' | 'playfair' | 'jetbrains' | 'system'
autoSave?: boolean
aiProcessingConsent?: boolean
}
/** Only fields that exist on `UserAISettings` in Prisma (excludes e.g. `theme`, which lives on `User`). */
@@ -47,6 +48,7 @@ const USER_AI_SETTINGS_PRISMA_KEYS = [
'noteHistoryMode',
'fontFamily',
'autoSave',
'aiProcessingConsent',
] as const
type UserAISettingsPrismaKey = (typeof USER_AI_SETTINGS_PRISMA_KEYS)[number]
@@ -150,6 +152,7 @@ const getCachedAISettings = unstable_cache(
noteHistoryMode: 'manual' as const,
fontFamily: 'inter' as const,
autoSave: true,
aiProcessingConsent: false,
}
}
@@ -175,6 +178,7 @@ const getCachedAISettings = unstable_cache(
noteHistoryMode: (settings.noteHistoryMode ?? 'manual') as 'manual' | 'auto',
fontFamily: (settings.fontFamily || 'inter') as 'inter' | 'playfair' | 'jetbrains' | 'system',
autoSave: settings.autoSave ?? true,
aiProcessingConsent: settings.aiProcessingConsent ?? false,
}
} catch (error) {
console.error('Error getting AI settings:', error)
@@ -200,6 +204,7 @@ const getCachedAISettings = unstable_cache(
noteHistoryMode: 'manual' as const,
fontFamily: 'inter' as const,
autoSave: true,
aiProcessingConsent: false,
}
}
},
@@ -238,6 +243,7 @@ export async function getAISettings(userId?: string) {
noteHistoryMode: 'manual' as const,
fontFamily: 'inter' as const,
autoSave: true,
aiProcessingConsent: false,
}
}

View File

@@ -30,7 +30,10 @@ function sanitizeSvgMarkup(svg: string): string {
* Génère une miniature SVG abstraite pour le flux éditorial (via modèle chat configuré).
* Respecte les préférences utilisateur (assistant IA activé) et nettoie le SVG.
*/
export async function generateNoteIllustrationSvg(noteId: string): Promise<{ ok: true } | { ok: false; error: string }> {
export async function generateNoteIllustrationSvg(
noteId: string,
options?: { skipRevalidation?: boolean },
): Promise<{ ok: true } | { ok: false; error: string }> {
const session = await auth()
if (!session?.user?.id) return { ok: false, error: 'Non autorisé' }
@@ -119,7 +122,9 @@ ${plainBody.slice(0, 200)}`
},
})
revalidatePath('/home')
if (!options?.skipRevalidation) {
revalidatePath('/home')
}
return { ok: true }
} catch (e) {
console.error('[note-illustration]', e)

View File

@@ -255,7 +255,7 @@ export async function getPendingShareCount() {
}
}
export async function leaveSharedNote(noteId: string) {
export async function leaveSharedNote(noteId: string, options?: { skipRevalidation?: boolean }) {
const session = await auth()
if (!session?.user?.id) throw new Error('Unauthorized')
@@ -268,7 +268,9 @@ export async function leaveSharedNote(noteId: string) {
await prisma.noteShare.update({ where: { id: share.id }, data: { status: 'removed' } })
revalidatePath('/home')
if (!options?.skipRevalidation) {
revalidatePath('/home')
}
return { success: true }
} catch (error: unknown) {
console.error('Error leaving shared note:', error)

View File

@@ -7,6 +7,7 @@ import { auth } from '@/auth'
import { getAIProvider } from '@/lib/ai/factory'
import { parseNote, getHashColor } from '@/lib/utils'
import { upsertNoteEmbedding } from '@/lib/embeddings'
import { syncNoteLinksForNote } from '@/lib/notes/sync-note-links'
import { getSystemConfig, getConfigNumber, getConfigBoolean, SEARCH_DEFAULTS } from '@/lib/config'
import { contextualAutoTagService } from '@/lib/ai/services/contextual-auto-tag.service'
import { semanticSearchService } from '@/lib/ai/services/semantic-search.service'
@@ -572,18 +573,18 @@ export async function updateNote(id: string, data: {
if (data.content !== undefined) {
const noteId = id
const content = data.content
; (async () => {
try {
const provider = getAIProvider(await getSystemConfig());
const embedding = await provider.getEmbeddings(content);
if (embedding) {
await upsertNoteEmbedding(noteId, embedding)
}
} catch (e) {
console.error('[BG] Embedding regeneration failed:', e);
const content = data.content;
(async () => {
try {
const provider = getAIProvider(await getSystemConfig());
const embedding = await provider.getEmbeddings(content);
if (embedding) {
await upsertNoteEmbedding(noteId, embedding);
}
}).catch(e => console.error('[BG] Uncaught background error:', e))
} catch (e) {
console.error('[BG] Embedding regeneration failed:', e);
}
})();
}
if ('checkItems' in data) updateData.checkItems = data.checkItems ? JSON.stringify(data.checkItems) : null
@@ -655,6 +656,12 @@ export async function updateNote(id: string, data: {
console.error('[HISTORY] Failed to create snapshot after update:', snapshotError)
}
if (data.content !== undefined) {
syncNoteLinksForNote(id, session.user.id, data.content).catch(err => {
console.error('[NoteLink] sync failed after updateNote:', err)
})
}
// Only revalidate for STRUCTURAL changes that affect the page layout/lists
// Content edits (title, content, size, color) use optimistic UI — no refresh needed
const structuralFields = ['isPinned', 'isArchived', 'labels', 'notebookId']
@@ -665,19 +672,19 @@ export async function updateNote(id: string, data: {
if (!options?.skipRevalidation) {
try { revalidatePath(`/note/${id}`) } catch {}
try { revalidatePath('/home') } catch {}
}
if (isStructuralChange) {
if (data.isArchived !== undefined) {
revalidatePath('/archive')
}
if (data.notebookId !== undefined && data.notebookId !== oldNotebookId) {
if (oldNotebookId) {
revalidatePath(`/notebook/${oldNotebookId}`)
if (isStructuralChange) {
if (data.isArchived !== undefined) {
revalidatePath('/archive')
}
if (data.notebookId) {
revalidatePath(`/notebook/${data.notebookId}`)
if (data.notebookId !== undefined && data.notebookId !== oldNotebookId) {
if (oldNotebookId) {
revalidatePath(`/notebook/${oldNotebookId}`)
}
if (data.notebookId) {
revalidatePath(`/notebook/${data.notebookId}`)
}
}
}
}
@@ -695,8 +702,20 @@ export async function updateNote(id: string, data: {
}
// Toggle functions
export async function togglePin(id: string, isPinned: boolean) { return updateNote(id, { isPinned }) }
export async function toggleArchive(id: string, isArchived: boolean) { return updateNote(id, { isArchived }) }
export async function togglePin(
id: string,
isPinned: boolean,
options?: { skipRevalidation?: boolean }
) {
return updateNote(id, { isPinned }, options)
}
export async function toggleArchive(
id: string,
isArchived: boolean,
options?: { skipRevalidation?: boolean }
) {
return updateNote(id, { isArchived }, options)
}
export async function updateColor(id: string, color: string) { return updateNote(id, { color }) }
export async function updateLabels(id: string, labels: string[]) { return updateNote(id, { labels }) }
export async function removeFusedBadge(id: string) { return updateNote(id, { autoGenerated: null, aiProvider: null }) }

View File

@@ -62,6 +62,7 @@ const getCachedUserSettings = unstable_cache(
return {
theme: 'light' as const satisfies ThemeId,
cardSizeMode: 'variable' as const,
accentColor: '#A47148',
}
}
},