feat(notes): liens internes, onglet Réseau, living blocks et consentement IA
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:
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 }) }
|
||||
|
||||
@@ -62,6 +62,7 @@ const getCachedUserSettings = unstable_cache(
|
||||
return {
|
||||
theme: 'light' as const satisfies ThemeId,
|
||||
cardSizeMode: 'variable' as const,
|
||||
accentColor: '#A47148',
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user