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

@@ -21,20 +21,40 @@ import { LabelBadge } from '@/components/label-badge'
import { NoteAttachments } from '@/components/note-attachments'
import { DocumentQAOverlay } from '@/components/document-qa-overlay'
import { useLanguage } from '@/lib/i18n'
import { useAiConsent } from '@/components/legal/ai-consent-provider'
import { useState } from 'react'
import { WikilinksBacklinksPanel } from '@/components/wikilinks-backlinks-panel'
import { MemoryEchoSection } from '@/components/memory-echo-section'
import { useRouter } from 'next/navigation'
interface NoteEditorFullPageProps {
onClose: () => void
}
export function NoteEditorFullPage({ onClose }: NoteEditorFullPageProps) {
const router = useRouter()
const { t, language } = useLanguage()
const { requestAiConsent } = useAiConsent()
const dateLocale = language === 'fr' ? fr : enUS
const { state, actions, note, readOnly, notebooks, fileInputRef, globalLabels } = useNoteEditorContext()
const [docQAAttachment, setDocQAAttachment] = useState<{ id: string; fileName: string } | null>(null)
const [attachmentsCount, setAttachmentsCount] = useState(0)
const [uploadTrigger, setUploadTrigger] = useState(0)
const [comparisonSimilarity, setComparisonSimilarity] = useState<number | undefined>()
const fetchNotesByIds = async (noteIds: string[]) => {
const notes = await Promise.all(noteIds.map(async (id) => {
try {
const res = await fetch(`/api/notes/${id}`)
if (!res.ok) return null
const data = await res.json()
return data.success && data.data ? data.data as Partial<Note> : null
} catch {
return null
}
}))
return notes.filter((n): n is Partial<Note> => n !== null)
}
const notebookName = notebooks.find(nb => nb.id === note.notebookId)?.name || null
@@ -109,6 +129,19 @@ export function NoteEditorFullPage({ onClose }: NoteEditorFullPageProps) {
<div className="max-w-3xl mx-auto w-full space-y-8 pb-32">
<NoteContentArea />
{!readOnly && (
<MemoryEchoSection
noteId={note.id}
onCompareNotes={async (noteIds, meta) => {
setComparisonSimilarity(meta?.similarity)
actions.setComparisonNotes(await fetchNotesByIds(noteIds))
}}
onMergeNotes={async (noteIds) => {
actions.setFusionNotes(await fetchNotesByIds(noteIds))
}}
/>
)}
<NoteAttachments
noteId={note.id}
onOpenDocQA={(att) => setDocQAAttachment(att)}
@@ -149,6 +182,8 @@ export function NoteEditorFullPage({ onClose }: NoteEditorFullPageProps) {
toast.error(t('ai.titleGenerationMinWords', { count: wordCount }))
return
}
const consented = await requestAiConsent()
if (!consented) return
actions.setIsProcessingAI(true)
try {
const res = await fetch('/api/ai/title-suggestions', {
@@ -209,6 +244,55 @@ export function NoteEditorFullPage({ onClose }: NoteEditorFullPageProps) {
onSave={actions.handleReminderSave}
onRemove={actions.handleRemoveReminder}
/>
{state.comparisonNotes.length > 0 && (
<ComparisonModal
isOpen
onClose={() => {
setComparisonSimilarity(undefined)
actions.setComparisonNotes([])
}}
notes={state.comparisonNotes}
similarity={comparisonSimilarity}
onMergeNotes={async (noteIds) => {
actions.setFusionNotes(await fetchNotesByIds(noteIds))
}}
/>
)}
{state.fusionNotes.length > 0 && (
<FusionModal
isOpen
onClose={() => actions.setFusionNotes([])}
notes={state.fusionNotes}
onConfirmFusion={async ({ title, content }, options) => {
await actions.handleSaveInPlace()
const { createNote, updateNote } = await import('@/app/actions/notes')
await createNote({
title,
content,
labels: options.keepAllTags
? [...new Set(state.fusionNotes.flatMap(n => n.labels || []))]
: state.fusionNotes[0].labels || [],
color: state.fusionNotes[0].color,
type: 'text',
isMarkdown: true,
autoGenerated: true,
aiProvider: 'fusion',
notebookId: state.fusionNotes[0].notebookId ?? undefined,
})
if (options.archiveOriginals) {
for (const fusionNote of state.fusionNotes) {
if (fusionNote.id) {
await updateNote(fusionNote.id, { isArchived: true })
}
}
}
toast.success(t('toast.notesFusionSuccess'))
actions.setFusionNotes([])
}}
/>
)}
</>
)
}