331 lines
12 KiB
TypeScript

'use client'
import { useState, useEffect, useCallback } from 'react'
import { useSearchParams, useRouter } from 'next/navigation'
import { Note } from '@/lib/types'
import { getAllNotes, getPinnedNotes, getRecentNotes, searchNotes } from '@/app/actions/notes'
import { getAISettings } from '@/app/actions/ai-settings'
import { NoteInput } from '@/components/note-input'
import { MasonryGrid } from '@/components/masonry-grid'
import { MemoryEchoNotification } from '@/components/memory-echo-notification'
import { NotebookSuggestionToast } from '@/components/notebook-suggestion-toast'
import { NoteEditor } from '@/components/note-editor'
import { BatchOrganizationDialog } from '@/components/batch-organization-dialog'
import { AutoLabelSuggestionDialog } from '@/components/auto-label-suggestion-dialog'
import { FavoritesSection } from '@/components/favorites-section'
import { RecentNotesSection } from '@/components/recent-notes-section'
import { Button } from '@/components/ui/button'
import { Wand2 } from 'lucide-react'
import { useLabels } from '@/context/LabelContext'
import { useNoteRefresh } from '@/context/NoteRefreshContext'
import { useReminderCheck } from '@/hooks/use-reminder-check'
import { useAutoLabelSuggestion } from '@/hooks/use-auto-label-suggestion'
export default function HomePage() {
console.log('[HomePage] Component rendering')
const searchParams = useSearchParams()
const router = useRouter()
const [notes, setNotes] = useState<Note[]>([])
const [pinnedNotes, setPinnedNotes] = useState<Note[]>([])
const [recentNotes, setRecentNotes] = useState<Note[]>([])
const [showRecentNotes, setShowRecentNotes] = useState(false)
const [editingNote, setEditingNote] = useState<{ note: Note; readOnly?: boolean } | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [notebookSuggestion, setNotebookSuggestion] = useState<{ noteId: string; content: string } | null>(null)
const [batchOrganizationOpen, setBatchOrganizationOpen] = useState(false)
const { refreshKey } = useNoteRefresh()
const { labels } = useLabels()
// Auto label suggestion (IA4)
const { shouldSuggest: shouldSuggestLabels, notebookId: suggestNotebookId, dismiss: dismissLabelSuggestion } = useAutoLabelSuggestion()
const [autoLabelOpen, setAutoLabelOpen] = useState(false)
// Open auto label dialog when suggestion is available
useEffect(() => {
if (shouldSuggestLabels && suggestNotebookId) {
setAutoLabelOpen(true)
}
}, [shouldSuggestLabels, suggestNotebookId])
// Check if viewing Notes générales (no notebook filter)
const notebookFilter = searchParams.get('notebook')
const isInbox = !notebookFilter
// Callback for NoteInput to trigger notebook suggestion and update UI
const handleNoteCreated = useCallback((note: Note) => {
console.log('[NotebookSuggestion] Note created:', { id: note.id, notebookId: note.notebookId, contentLength: note.content?.length })
// Update UI immediately by adding the note to the list if it matches current filters
setNotes((prevNotes) => {
// Check if note matches current filters
const notebookFilter = searchParams.get('notebook')
const labelFilter = searchParams.get('labels')?.split(',').filter(Boolean) || []
const colorFilter = searchParams.get('color')
const search = searchParams.get('search')?.trim() || null
// Check notebook filter
if (notebookFilter && note.notebookId !== notebookFilter) {
return prevNotes // Note doesn't match notebook filter
}
if (!notebookFilter && note.notebookId) {
return prevNotes // Viewing inbox but note has notebook
}
// Check label filter
if (labelFilter.length > 0) {
const noteLabels = note.labels || []
if (!noteLabels.some((label: string) => labelFilter.includes(label))) {
return prevNotes // Note doesn't match label filter
}
}
// Check color filter
if (colorFilter) {
const labelNamesWithColor = labels
.filter((label: any) => label.color === colorFilter)
.map((label: any) => label.name)
const noteLabels = note.labels || []
if (!noteLabels.some((label: string) => labelNamesWithColor.includes(label))) {
return prevNotes // Note doesn't match color filter
}
}
// Check search filter (simple check - if searching, let refresh handle it)
if (search) {
// If searching, refresh to get proper search results
router.refresh()
return prevNotes
}
// Note matches all filters - add it optimistically to the beginning of the list
// (newest notes first based on order: isPinned desc, order asc, updatedAt desc)
const isPinned = note.isPinned || false
const pinnedNotes = prevNotes.filter(n => n.isPinned)
const unpinnedNotes = prevNotes.filter(n => !n.isPinned)
if (isPinned) {
// Add to beginning of pinned notes
return [note, ...pinnedNotes, ...unpinnedNotes]
} else {
// Add to beginning of unpinned notes
return [...pinnedNotes, note, ...unpinnedNotes]
}
})
// Only suggest if note has no notebook and has 20+ words
if (!note.notebookId) {
const wordCount = (note.content || '').trim().split(/\s+/).filter(w => w.length > 0).length
console.log('[NotebookSuggestion] Word count:', wordCount)
if (wordCount >= 20) {
console.log('[NotebookSuggestion] Triggering suggestion for note:', note.id)
setNotebookSuggestion({
noteId: note.id,
content: note.content || ''
})
} else {
console.log('[NotebookSuggestion] Not enough words, need 20+')
}
} else {
console.log('[NotebookSuggestion] Note has notebook, skipping')
}
// Refresh in background to ensure data consistency (non-blocking)
router.refresh()
}, [searchParams, labels, router])
const handleOpenNote = (noteId: string) => {
const note = notes.find(n => n.id === noteId)
if (note) {
setEditingNote({ note, readOnly: false })
}
}
// Enable reminder notifications
useReminderCheck(notes)
// Load user settings to check if recent notes should be shown
useEffect(() => {
const loadSettings = async () => {
try {
const settings = await getAISettings()
setShowRecentNotes(settings.showRecentNotes === true)
} catch (error) {
setShowRecentNotes(false)
}
}
loadSettings()
}, [refreshKey])
useEffect(() => {
const loadNotes = async () => {
setIsLoading(true)
const search = searchParams.get('search')?.trim() || null
const semanticMode = searchParams.get('semantic') === 'true'
const labelFilter = searchParams.get('labels')?.split(',').filter(Boolean) || []
const colorFilter = searchParams.get('color')
const notebookFilter = searchParams.get('notebook')
let allNotes = search ? await searchNotes(search, semanticMode, notebookFilter || undefined) : await getAllNotes()
// Filter by selected notebook
if (notebookFilter) {
allNotes = allNotes.filter((note: any) => note.notebookId === notebookFilter)
} else {
// If no notebook selected, only show notes without notebook (Notes générales)
allNotes = allNotes.filter((note: any) => !note.notebookId)
}
// Filter by selected labels
if (labelFilter.length > 0) {
allNotes = allNotes.filter((note: any) =>
note.labels?.some((label: string) => labelFilter.includes(label))
)
}
// Filter by color (filter notes that have labels with this color)
// Note: We use a ref-like pattern to avoid including labels in dependencies
// This prevents dialog closing when adding new labels
if (colorFilter) {
const labelNamesWithColor = labels
.filter((label: any) => label.color === colorFilter)
.map((label: any) => label.name)
allNotes = allNotes.filter((note: any) =>
note.labels?.some((label: string) => labelNamesWithColor.includes(label))
)
}
// Load pinned notes separately (shown in favorites section)
const pinned = await getPinnedNotes()
// Filter pinned notes by current filters as well
if (notebookFilter) {
setPinnedNotes(pinned.filter((note: any) => note.notebookId === notebookFilter))
} else {
// If no notebook selected, only show pinned notes without notebook
setPinnedNotes(pinned.filter((note: any) => !note.notebookId))
}
// Load recent notes only if enabled in settings
if (showRecentNotes) {
const recent = await getRecentNotes(3)
// Filter recent notes by current filters
if (notebookFilter) {
setRecentNotes(recent.filter((note: any) => note.notebookId === notebookFilter))
} else {
setRecentNotes(recent.filter((note: any) => !note.notebookId))
}
} else {
setRecentNotes([])
}
setNotes(allNotes)
setIsLoading(false)
}
loadNotes()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchParams, refreshKey, showRecentNotes]) // Intentionally omit 'labels' and 'semantic' to prevent reload when adding tags or from router.push
return (
<main className="container mx-auto px-4 py-8 max-w-7xl">
<NoteInput onNoteCreated={handleNoteCreated} />
{/* Batch Organization Button - Only show in Inbox with 5+ notes */}
{isInbox && !isLoading && notes.length >= 5 && (
<div className="mb-4 flex justify-end">
<Button
onClick={() => setBatchOrganizationOpen(true)}
variant="default"
className="gap-2"
>
<Wand2 className="h-4 w-4" />
Organiser avec l'IA ({notes.length})
</Button>
</div>
)}
{isLoading ? (
<div className="text-center py-8 text-gray-500">Loading...</div>
) : (
<>
{/* Favorites Section - Pinned Notes */}
<FavoritesSection
pinnedNotes={pinnedNotes}
onEdit={(note, readOnly) => setEditingNote({ note, readOnly })}
/>
{/* Recent Notes Section - Only shown if enabled in settings */}
{showRecentNotes && (
<RecentNotesSection
recentNotes={recentNotes.filter(note => !note.isPinned)}
onEdit={(note, readOnly) => setEditingNote({ note, readOnly })}
/>
)}
{/* Main Notes Grid - Unpinned Notes Only */}
{notes.filter(note => !note.isPinned).length > 0 && (
<div data-testid="notes-grid">
<MasonryGrid
notes={notes.filter(note => !note.isPinned)}
onEdit={(note, readOnly) => setEditingNote({ note, readOnly })}
/>
</div>
)}
{/* Empty state when no notes */}
{notes.filter(note => !note.isPinned).length === 0 && pinnedNotes.length === 0 && (
<div className="text-center py-8 text-gray-500">
No notes yet. Create your first note!
</div>
)}
</>
)}
{/* Memory Echo - Proactive note connections */}
<MemoryEchoNotification onOpenNote={handleOpenNote} />
{/* Notebook Suggestion - IA1 */}
{notebookSuggestion && (
<NotebookSuggestionToast
noteId={notebookSuggestion.noteId}
noteContent={notebookSuggestion.content}
onDismiss={() => setNotebookSuggestion(null)}
/>
)}
{/* Batch Organization Dialog - IA3 */}
<BatchOrganizationDialog
open={batchOrganizationOpen}
onOpenChange={setBatchOrganizationOpen}
onNotesMoved={() => {
// Refresh notes to see updated notebook assignments
router.refresh()
}}
/>
{/* Auto Label Suggestion Dialog - IA4 */}
<AutoLabelSuggestionDialog
open={autoLabelOpen}
onOpenChange={(open) => {
setAutoLabelOpen(open)
if (!open) dismissLabelSuggestion()
}}
notebookId={suggestNotebookId}
onLabelsCreated={() => {
// Refresh to see new labels
router.refresh()
}}
/>
{/* Note Editor Modal */}
{editingNote && (
<NoteEditor
note={editingNote.note}
readOnly={editingNote.readOnly}
onClose={() => setEditingNote(null)}
/>
)}
</main>
)
}