feat: editor improvements and architectural grid prototype
Multiple feature additions and improvements across the application: - NextGen Editor: drag handles, smart paste, block actions - Structured views: Kanban and table layouts for notes - Architectural Grid: new brainstorming/agent interface prototype - Flashcards: SM-2 revision algorithm with AI generation - MCP server: robustness improvements - Graph/PDF chat: fix click propagation and copy behavior - Various UI/UX enhancements and bug fixes Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -40,7 +40,7 @@ import { useSearchModal } from '@/context/search-modal-context'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { applyDocumentTheme } from '@/lib/apply-document-theme'
|
||||
import { getAllNotes, getTrashCount } from '@/app/actions/notes'
|
||||
import { getAllNotes, getTrashCount, getNotesWithReminders } from '@/app/actions/notes'
|
||||
import { NOTE_CHANGE_EVENT, type NoteChangeEvent } from '@/lib/note-change-sync'
|
||||
import { useNotebooks } from '@/context/notebooks-context'
|
||||
import { Notebook, Note } from '@/lib/types'
|
||||
@@ -60,7 +60,7 @@ import { performSignOut } from '@/lib/auth-client'
|
||||
import { useBrainstormSessions, useDeleteBrainstorm } from '@/hooks/use-brainstorm'
|
||||
import { UsageMeter } from './usage-meter'
|
||||
|
||||
type NavigationView = 'notebooks' | 'agents' | 'reminders' | 'brainstorms' | 'revision'
|
||||
type NavigationView = 'notebooks' | 'agents' | 'reminders' | 'brainstorms' | 'revision' | 'insights'
|
||||
type SortOrder = 'newest' | 'oldest' | 'alpha' | 'manual'
|
||||
|
||||
function NoteLink({
|
||||
@@ -175,6 +175,96 @@ function SidebarBrainstorms() {
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarReminders({ onOpenNote }: { onOpenNote: (noteId: string, notebookId: string | null) => void }) {
|
||||
const { t } = useLanguage()
|
||||
const [reminders, setReminders] = useState<
|
||||
{ id: string; title: string | null; reminder: Date | string | null; isReminderDone: boolean; notebookId: string | null }[]
|
||||
>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
setLoading(true)
|
||||
getNotesWithReminders()
|
||||
.then((rows) => {
|
||||
if (!cancelled) setReminders(rows as typeof reminders)
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoading(false)
|
||||
})
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="px-4 space-y-2">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="h-10 rounded-xl bg-paper/50 animate-pulse" />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
const active = reminders.filter((r) => !r.isReminderDone && r.reminder)
|
||||
const overdue = active.filter((r) => new Date(r.reminder!) < now)
|
||||
const upcoming = active.filter((r) => new Date(r.reminder!) >= now)
|
||||
|
||||
if (active.length === 0) {
|
||||
return (
|
||||
<div className="px-4 py-8 text-center border border-dashed border-border rounded-2xl bg-paper/30 mx-4">
|
||||
<Clock size={24} className="mx-auto text-concrete/40 mb-3" />
|
||||
<p className="text-[11px] text-concrete italic">{t('reminders.emptyDescription')}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const renderItem = (note: (typeof active)[0], overdueItem?: boolean) => (
|
||||
<button
|
||||
key={note.id}
|
||||
type="button"
|
||||
onClick={() => onOpenNote(note.id, note.notebookId)}
|
||||
className="w-full text-start px-4 py-2.5 rounded-xl hover:bg-brand-accent/5 transition-colors group"
|
||||
>
|
||||
<p className="text-[12px] font-medium truncate group-hover:text-brand-accent transition-colors">
|
||||
{note.title || t('notes.untitled')}
|
||||
</p>
|
||||
<p className={cn('text-[10px] mt-0.5', overdueItem ? 'text-red-500' : 'text-muted-foreground')}>
|
||||
{note.reminder &&
|
||||
new Date(note.reminder).toLocaleString(undefined, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})}
|
||||
</p>
|
||||
</button>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="space-y-4 pb-2">
|
||||
{overdue.length > 0 && (
|
||||
<div>
|
||||
<p className="text-[9px] font-bold uppercase tracking-widest text-red-500 px-4 mb-1">
|
||||
{t('reminders.overdue')}
|
||||
</p>
|
||||
<div className="space-y-0.5">{overdue.map((n) => renderItem(n, true))}</div>
|
||||
</div>
|
||||
)}
|
||||
{upcoming.length > 0 && (
|
||||
<div>
|
||||
<p className="text-[9px] font-bold uppercase tracking-widest text-muted-foreground px-4 mb-1">
|
||||
{t('reminders.upcoming')}
|
||||
</p>
|
||||
<div className="space-y-0.5">{upcoming.map((n) => renderItem(n))}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarCarnetItem({
|
||||
carnet,
|
||||
isActive,
|
||||
@@ -536,8 +626,19 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
useEffect(() => {
|
||||
if (pathname.startsWith('/brainstorm')) setActiveView('brainstorms')
|
||||
else if (pathname.startsWith('/agents') || pathname.startsWith('/lab')) setActiveView('agents')
|
||||
else setActiveView('notebooks')
|
||||
}, [pathname])
|
||||
else if (pathname === '/insights') setActiveView('insights')
|
||||
else if (pathname.startsWith('/revision')) setActiveView('revision')
|
||||
else if (searchParams.get('reminders') === '1' && pathname === '/home') setActiveView('reminders')
|
||||
else if (pathname === '/home' || pathname.startsWith('/notes')) setActiveView('notebooks')
|
||||
}, [pathname, searchParams])
|
||||
|
||||
const isRemindersRoute = pathname === '/home' && searchParams.get('reminders') === '1'
|
||||
const isSharedRoute = pathname === '/home' && searchParams.get('shared') === '1'
|
||||
const isNotebooksRoute =
|
||||
(pathname === '/home' || pathname.startsWith('/notes')) &&
|
||||
!pathname.startsWith('/settings') &&
|
||||
!isRemindersRoute &&
|
||||
!isSharedRoute
|
||||
|
||||
const displayName = user?.name || user?.email || ''
|
||||
const initial = displayName ? displayName.charAt(0).toUpperCase() : '?'
|
||||
@@ -645,6 +746,19 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
router.push(`/home?${params.toString()}`)
|
||||
}
|
||||
|
||||
const handleRemindersClick = () => {
|
||||
setActiveView('reminders')
|
||||
router.push('/home?reminders=1&forceList=1')
|
||||
}
|
||||
|
||||
const handleReminderNoteClick = (noteId: string, notebookId: string | null) => {
|
||||
const params = new URLSearchParams()
|
||||
params.set('reminders', '1')
|
||||
if (notebookId) params.set('notebook', notebookId)
|
||||
params.set('openNote', noteId)
|
||||
router.push(`/home?${params.toString()}`)
|
||||
}
|
||||
|
||||
const handleInboxClick = () => {
|
||||
router.push('/home?forceList=1')
|
||||
}
|
||||
@@ -972,14 +1086,59 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
{/* Boutons de navigation principaux */}
|
||||
<div className="flex flex-col gap-1.5 w-full px-1.5">
|
||||
{([
|
||||
{ id: 'notebooks', icon: BookOpen, label: t('nav.notebooks'), onClick: () => { setActiveView('notebooks'); if (pathname !== '/home') router.push('/home') }, isActive: activeView === 'notebooks' && !pathname.startsWith('/settings') },
|
||||
{ id: 'insights', icon: Sparkles, label: t('nav.insights'), onClick: () => router.push('/insights'), isActive: pathname === '/insights' },
|
||||
{ id: 'revision', icon: GraduationCap, label: t('nav.revision'), onClick: () => router.push('/revision'), isActive: pathname === '/revision' },
|
||||
{ id: 'agents', icon: Bot, label: t('agents.intelligenceOS') || 'Intelligence IA', onClick: () => { setActiveView('agents'); router.push('/agents') }, isActive: activeView === 'agents' || (pathname.startsWith('/agents') && activeView !== 'notebooks') },
|
||||
{ id: 'reminders', icon: Bell, label: t('sidebar.reminders'), onClick: () => setActiveView('reminders'), isActive: activeView === 'reminders' },
|
||||
{
|
||||
id: 'notebooks',
|
||||
icon: BookOpen,
|
||||
label: t('nav.notebooks'),
|
||||
onClick: () => {
|
||||
setActiveView('notebooks')
|
||||
router.push('/home?forceList=1')
|
||||
},
|
||||
isActive: isNotebooksRoute,
|
||||
},
|
||||
{
|
||||
id: 'insights',
|
||||
icon: Sparkles,
|
||||
label: t('nav.insights'),
|
||||
onClick: () => {
|
||||
setActiveView('insights')
|
||||
router.push('/insights')
|
||||
},
|
||||
isActive: pathname === '/insights',
|
||||
},
|
||||
{
|
||||
id: 'revision',
|
||||
icon: GraduationCap,
|
||||
label: t('nav.revision'),
|
||||
onClick: () => {
|
||||
setActiveView('revision')
|
||||
router.push('/revision')
|
||||
},
|
||||
isActive: pathname.startsWith('/revision'),
|
||||
},
|
||||
{
|
||||
id: 'agents',
|
||||
icon: Bot,
|
||||
label: t('agents.intelligenceOS') || 'Intelligence IA',
|
||||
onClick: () => {
|
||||
setActiveView('agents')
|
||||
router.push('/agents')
|
||||
},
|
||||
isActive: pathname.startsWith('/agents') || pathname.startsWith('/lab'),
|
||||
},
|
||||
{
|
||||
id: 'reminders',
|
||||
icon: Bell,
|
||||
label: t('sidebar.reminders'),
|
||||
onClick: handleRemindersClick,
|
||||
isActive: isRemindersRoute,
|
||||
},
|
||||
] as { id: string; icon: React.FC<{ size?: number }>; label: string; onClick: () => void; isActive: boolean }[]).map(item => (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
aria-label={item.label}
|
||||
aria-current={item.isActive ? 'page' : undefined}
|
||||
onClick={item.onClick}
|
||||
className={cn(
|
||||
'w-9 h-9 rounded-lg flex items-center justify-center transition-all relative group',
|
||||
@@ -992,7 +1151,10 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-[3px] h-4 bg-brand-accent rounded-r-full" />
|
||||
)}
|
||||
<item.icon size={16} />
|
||||
<span className="absolute left-[50px] top-1/2 -translate-y-1/2 bg-ink dark:bg-white dark:text-ink text-paper text-[9px] font-bold py-1 px-2 rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-50 pointer-events-none shadow-md uppercase tracking-wider">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="absolute left-[50px] top-1/2 -translate-y-1/2 bg-ink dark:bg-white dark:text-ink text-paper text-[9px] font-bold py-1 px-2 rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-50 pointer-events-none shadow-md uppercase tracking-wider"
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
</button>
|
||||
@@ -1232,6 +1394,60 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
) : activeView === 'insights' ? (
|
||||
<motion.div
|
||||
key="insights"
|
||||
initial={{ opacity: 0, x: isRtl ? 10 : -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: isRtl ? -10 : 10 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="px-4 pt-4 space-y-4"
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Sparkles size={14} className="text-brand-accent" />
|
||||
<h3 className="text-xs font-black tracking-widest uppercase text-ink dark:text-dark-ink">
|
||||
{t('nav.insights')}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-[12px] leading-relaxed text-muted-foreground">
|
||||
{t('sidebar.insightsPanelBody')}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.push('/home')}
|
||||
className="w-full flex items-center gap-2 px-3 py-2.5 rounded-xl border border-border/40 bg-white/60 dark:bg-zinc-800/40 hover:border-brand-accent/30 hover:bg-brand-accent/5 transition-all text-[12px] font-medium text-foreground"
|
||||
>
|
||||
<BookOpen size={14} className="text-brand-accent shrink-0" />
|
||||
{t('sidebar.backToNotebooks')}
|
||||
</button>
|
||||
</motion.div>
|
||||
) : activeView === 'revision' ? (
|
||||
<motion.div
|
||||
key="revision"
|
||||
initial={{ opacity: 0, x: isRtl ? 10 : -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: isRtl ? -10 : 10 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="px-4 pt-4 space-y-4"
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<GraduationCap size={14} className="text-brand-accent" />
|
||||
<h3 className="text-xs font-black tracking-widest uppercase text-ink dark:text-dark-ink">
|
||||
{t('nav.revision')}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-[12px] leading-relaxed text-muted-foreground">
|
||||
{t('sidebar.revisionPanelBody')}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.push('/home')}
|
||||
className="w-full flex items-center gap-2 px-3 py-2.5 rounded-xl border border-border/40 bg-white/60 dark:bg-zinc-800/40 hover:border-brand-accent/30 hover:bg-brand-accent/5 transition-all text-[12px] font-medium text-foreground"
|
||||
>
|
||||
<BookOpen size={14} className="text-brand-accent shrink-0" />
|
||||
{t('sidebar.backToNotebooks')}
|
||||
</button>
|
||||
</motion.div>
|
||||
) : activeView === 'reminders' ? (
|
||||
<motion.div
|
||||
key="reminders"
|
||||
@@ -1239,13 +1455,16 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: isRtl ? -10 : 10 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="flex flex-col min-h-0"
|
||||
>
|
||||
<p className="text-[10px] font-bold text-muted-ink tracking-[0.2em] uppercase mb-4 px-4">
|
||||
{t('sidebar.reminders')}
|
||||
</p>
|
||||
<div className="px-4 py-8 text-center border border-dashed border-border rounded-2xl bg-paper/30">
|
||||
<Clock size={24} className="mx-auto text-concrete/40 mb-3" />
|
||||
<p className="text-[11px] text-concrete italic">{t('sidebar.noReminders')}</p>
|
||||
<div className="flex items-center gap-1.5 px-4 pt-4 mb-3 shrink-0">
|
||||
<Bell size={14} className="text-brand-accent" />
|
||||
<h3 className="text-xs font-black tracking-widest uppercase text-ink dark:text-dark-ink">
|
||||
{t('sidebar.reminders')}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto custom-scrollbar min-h-0">
|
||||
<SidebarReminders onOpenNote={handleReminderNoteClick} />
|
||||
</div>
|
||||
</motion.div>
|
||||
) : activeView === 'agents' ? (
|
||||
|
||||
Reference in New Issue
Block a user