All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 1m24s
178 lines
7.7 KiB
TypeScript
178 lines
7.7 KiB
TypeScript
'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 { format } from 'date-fns'
|
||
import { ChevronRight } from 'lucide-react'
|
||
import { toast } from 'sonner'
|
||
import { Note } from '@/lib/types'
|
||
import { GhostTags } from '@/components/ghost-tags'
|
||
import { LabelBadge } from '@/components/label-badge'
|
||
|
||
interface NoteEditorFullPageProps {
|
||
onClose: () => void
|
||
}
|
||
|
||
export function NoteEditorFullPage({ onClose }: NoteEditorFullPageProps) {
|
||
const { state, actions, note, readOnly, notebooks, fileInputRef, globalLabels } = useNoteEditorContext()
|
||
|
||
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 ── */}
|
||
<div className="h-full flex items-stretch overflow-hidden transition-all duration-500">
|
||
|
||
{/* ── main scrollable column ── */}
|
||
<div className="flex-1 flex flex-col overflow-y-auto bg-white dark:bg-background">
|
||
|
||
{/* TOOLBAR */}
|
||
<NoteEditorToolbar mode="fullPage" onClose={onClose} />
|
||
|
||
{/* BODY — max-w-4xl, responsive px, py-16 */}
|
||
<div className="max-w-4xl mx-auto w-full px-6 sm:px-12 py-16 space-y-12 min-w-0">
|
||
|
||
{/* Breadcrumb + Title block */}
|
||
<div className="space-y-4">
|
||
{/* Breadcrumb: Notebook › Date */}
|
||
<div className="flex items-center gap-3 text-[12px] uppercase tracking-[.25em] font-bold">
|
||
{notebookName && <span style={{ color: 'var(--color-ink)' }}>{notebookName}</span>}
|
||
{notebookName && <ChevronRight size={10} style={{ color: 'var(--color-concrete)' }} />}
|
||
<span suppressHydrationWarning style={{ color: 'var(--color-concrete)' }}>
|
||
{format(new Date(note.contentUpdatedAt), 'MMM d, yyyy')}
|
||
</span>
|
||
</div>
|
||
|
||
{/* Title */}
|
||
<NoteTitleBlock />
|
||
|
||
{(state.labels.length > 0 || (state.filteredSuggestions.length > 0)) && (
|
||
<div className="flex flex-wrap gap-2 pt-2">
|
||
{state.labels.map((label) => (
|
||
<LabelBadge
|
||
key={label}
|
||
label={label}
|
||
type={getLabelType(label)}
|
||
onRemove={() => actions.handleRemoveLabel(label)}
|
||
/>
|
||
))}
|
||
{!readOnly && (
|
||
<GhostTags
|
||
suggestions={state.filteredSuggestions}
|
||
addedTags={state.labels}
|
||
isAnalyzing={state.isAnalyzingSuggestions}
|
||
onSelectTag={actions.handleSelectGhostTag}
|
||
onDismissTag={actions.handleDismissGhostTag}
|
||
/>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Hero image — show first note image if present */}
|
||
{state.allImages.length > 0 && (
|
||
<div className="aspect-[16/9] w-full bg-slate-100 dark:bg-zinc-900 rounded-xl overflow-hidden shadow-xl">
|
||
<img
|
||
src={state.allImages[0]}
|
||
alt={state.title}
|
||
className="w-full h-full object-cover grayscale contrast-110 hover:grayscale-0 transition-all duration-500"
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{/* Content area — max-w-3xl for wider reading column */}
|
||
<div className="max-w-3xl mx-auto w-full pb-32">
|
||
<NoteContentArea />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* ── Side panel: AI Chat ── */}
|
||
{state.aiOpen && (
|
||
<div className="h-full self-stretch bg-background flex flex-col z-50 shrink-0">
|
||
<ContextualAIChat
|
||
onClose={() => 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('Ajoutez au moins 10 mots avant de générer un titre.')
|
||
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('Titre généré !')
|
||
} else {
|
||
toast.error('Impossible de générer un titre.')
|
||
}
|
||
} else {
|
||
toast.error('Erreur lors de la génération du titre.')
|
||
}
|
||
} catch { toast.error('Erreur réseau.') } finally { actions.setIsProcessingAI(false) }
|
||
}}
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{/* ── Side panel: Document Info ── */}
|
||
{state.infoOpen && (
|
||
<div className="w-[400px] h-full self-stretch border-l border-black/10 dark:border-white/10 bg-background flex flex-col z-50 shrink-0">
|
||
<NoteDocumentInfoPanel
|
||
note={note}
|
||
content={state.content}
|
||
onClose={() => actions.setInfoOpen(false)}
|
||
onNoteRestored={(r: Note) => { actions.setContent(r.content || ''); actions.setTitle(r.title || ''); actions.setIsDirty(false) }}
|
||
/>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<input ref={fileInputRef} type="file" accept="image/*" multiple className="hidden" onChange={actions.handleImageUpload} />
|
||
<ReminderDialog
|
||
open={state.showReminderDialog}
|
||
onOpenChange={actions.setShowReminderDialog}
|
||
currentReminder={state.currentReminder}
|
||
onSave={actions.handleReminderSave}
|
||
onRemove={actions.handleRemoveReminder}
|
||
/>
|
||
</>
|
||
)
|
||
} |