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:
Antigravity
2026-05-27 19:45:15 +00:00
parent 2de66a863d
commit f46654f574
99 changed files with 29948 additions and 919 deletions

View File

@@ -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' ? (