'use client' import { useNoteEditorContext } from './note-editor-context' import { NoteEditorToolbar } from './note-editor-toolbar' import { NoteTitleBlock } from './note-title-block' import { NoteContentArea } from './note-content-area' import { EditorImages } from '@/components/editor-images' import { ComparisonModal } from '@/components/comparison-modal' import { FusionModal } from '@/components/fusion-modal' import { ReminderDialog } from '@/components/reminder-dialog' import { ContextualAIChat } from '@/components/contextual-ai-chat' import { NoteDocumentInfoPanel } from '@/components/note-document-info-panel' import { fr } from 'date-fns/locale/fr' import { enUS } from 'date-fns/locale/en-US' import { formatAbsoluteDateLocalized } from '@/lib/utils/format-localized-date' import { ChevronRight } from 'lucide-react' import { toast } from 'sonner' import { Note } from '@/lib/types' import { GhostTags } from '@/components/ghost-tags' import { InlinePaywall } from '@/components/settings/inline-paywall' 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' interface NoteEditorFullPageProps { onClose: () => void } export function NoteEditorFullPage({ onClose }: NoteEditorFullPageProps) { 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() 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 : null } catch { return null } })) return notes.filter((n): n is Partial => n !== null) } const notebookName = notebooks.find(nb => nb.id === note.notebookId)?.name || null const getLabelType = (name: string): 'ai' | 'user' => { const found = globalLabels.find(l => l.name.toLowerCase() === name.toLowerCase()) return (found as any)?.type === 'ai' ? 'ai' : 'user' } return ( <> {/* ── outer container ── */}
{/* ── main scrollable column ── */}
{/* TOOLBAR */} setUploadTrigger(v => v + 1)} attachmentsCount={attachmentsCount} /> {/* BODY — max-w-4xl, responsive px, py-16 */}
{/* Breadcrumb + Title block */}
{/* Breadcrumb: Notebook › Date */}
{notebookName && {notebookName}} {notebookName && } {formatAbsoluteDateLocalized(new Date(note.contentUpdatedAt), language, 'MMM d, yyyy', dateLocale)}
{/* Title */} {(state.labels.length > 0 || state.filteredSuggestions.length > 0 || state.quotaExceededFeature === 'auto_tag') && (
{state.labels.map((label) => ( actions.handleRemoveLabel(label)} /> ))} {!readOnly && ( state.quotaExceededFeature === 'auto_tag' ? ( actions.setQuotaExceededFeature(null)} /> ) : ( ) )}
)}
{/* Hero image — show first note image if present */} {state.allImages.length > 0 && (
{state.title}
)} {/* Content area — max-w-3xl for wider reading column */}
{state.quotaExceededFeature === 'reformulate' && ( actions.setQuotaExceededFeature(null)} /> )} {!readOnly && ( { setComparisonSimilarity(meta?.similarity) actions.setComparisonNotes(await fetchNotesByIds(noteIds)) }} onMergeNotes={async (noteIds) => { actions.setFusionNotes(await fetchNotesByIds(noteIds)) }} /> )} setDocQAAttachment(att)} onCountChange={setAttachmentsCount} triggerUpload={uploadTrigger} /> {/* WikiLinks backlinks */}
{/* ── Side panel: AI Chat ── */} {state.aiOpen && (
actions.setAiOpen(false)} noteTitle={state.title} noteContent={state.content} noteImages={state.allImages} noteId={note.id} onApplyToNote={(nc: string) => { actions.setPreviousContentForCopilot(state.content) actions.setContent(nc) if (state.isMarkdown) actions.setShowMarkdownPreview(true) }} onUndoLastAction={state.previousContentForCopilot !== null ? () => { actions.setContent(state.previousContentForCopilot!); actions.setPreviousContentForCopilot(null) } : undefined} lastActionApplied={state.previousContentForCopilot !== null} notebooks={notebooks} notebookId={note.notebookId ?? undefined} notebookName={notebookName ?? undefined} diagramInsertFormat={state.isMarkdown ? 'markdown' : 'html'} onGenerateTitle={async () => { const plain = state.content.replace(/<[^>]+>/g, ' ').trim() const wordCount = plain.split(/\s+/).filter(Boolean).length if (wordCount < 10) { 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', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: plain }), }) if (res.ok) { const data = await res.json() const s = data.suggestions?.[0]?.title ?? '' if (s) { actions.setTitle(s) toast.success(t('ai.titleApplied')) } else { toast.error(t('ai.titleGenerationFailed')) } } else { toast.error(t('ai.titleGenerationError')) } } catch { toast.error(t('ai.networkErrorShort')) } finally { actions.setIsProcessingAI(false) } }} />
)} {/* ── Side panel: Document Info ── */} {state.infoOpen && (
actions.setInfoOpen(false)} onNoteRestored={(r: Note) => { actions.setContent(r.content || ''); actions.setTitle(r.title || ''); actions.setIsDirty(false) }} />
)}
{docQAAttachment && ( setDocQAAttachment(null)} onApplyToNote={(content) => { actions.setPreviousContentForCopilot(state.content) actions.setContent(state.content + '\n\n' + content) }} /> )} {state.comparisonNotes.length > 0 && ( { setComparisonSimilarity(undefined) actions.setComparisonNotes([]) }} notes={state.comparisonNotes} similarity={comparisonSimilarity} onMergeNotes={async (noteIds) => { actions.setFusionNotes(await fetchNotesByIds(noteIds)) }} /> )} {state.fusionNotes.length > 0 && ( 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([]) }} /> )} ) }