feat: brainstorm sessions, PDF document Q&A, embedding fixes, and UI improvements
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 7s

- Add brainstorm feature with collaborative canvas, AI idea generation, live cursors, playback, and export
- Add PDF upload/extraction/ingestion pipeline with pgvector document search (RAG)
- Add document Q&A overlay with streaming chat and PDF preview
- Add note attachments UI with status polling, grid layout, and auto-scroll
- Add task extraction AI tool and agent executor improvements
- Fix NoteEmbedding missing updatedAt column, re-index 66 notes with 1536-dim embeddings
- Fix brainstorm 'Create Note' button: add success toast and redirect to created note
- Fix memory echo notification infinite polling
- Fix chat route to always include document_search tool
- Add brainstorm i18n keys across all 14 locales
- Add socket server for real-time brainstorm collaboration
- Add hierarchical notebook selector and organize notebook dialog improvements
- Add sidebar brainstorm section with session management
- Update prisma schema with brainstorm tables, attachments, and document chunks
This commit is contained in:
Antigravity
2026-05-14 17:43:21 +00:00
parent 195e845f0a
commit 1fcea6ed7d
228 changed files with 57656 additions and 1059 deletions

View File

@@ -27,6 +27,7 @@ import {
Sun,
Pin,
PinOff,
Sparkles,
} from 'lucide-react'
import { useLanguage } from '@/lib/i18n'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
@@ -48,8 +49,9 @@ import {
} from '@/components/ui/dropdown-menu'
import { signOut } from 'next-auth/react'
import { useNoteRefresh } from '@/context/NoteRefreshContext'
import { useBrainstormSessions, useDeleteBrainstorm } from '@/hooks/use-brainstorm'
type NavigationView = 'notebooks' | 'agents' | 'reminders'
type NavigationView = 'notebooks' | 'agents' | 'reminders' | 'brainstorms'
type SortOrder = 'newest' | 'oldest' | 'alpha'
function NoteLink({
@@ -61,13 +63,15 @@ function NoteLink({
isActive: boolean
onClick: () => void
}) {
const { language } = useLanguage()
const slideX = language === 'fa' || language === 'ar' ? 10 : -10
return (
<motion.button
initial={{ opacity: 0, x: -10 }}
initial={{ opacity: 0, x: slideX }}
animate={{ opacity: 1, x: 0 }}
onClick={onClick}
className={cn(
'w-full flex items-center gap-2 pl-12 pr-4 py-2 text-[12px] transition-colors rounded-lg text-left',
'w-full flex items-center gap-2 ps-12 pe-4 py-2 text-[12px] transition-colors rounded-lg text-start',
isActive ? 'bg-white/50 text-foreground font-medium' : 'text-muted-foreground hover:text-foreground hover:bg-white/30'
)}
>
@@ -80,6 +84,81 @@ function NoteLink({
)
}
function SidebarBrainstorms() {
const { data: sessions, isLoading } = useBrainstormSessions()
const deleteBrainstorm = useDeleteBrainstorm()
const router = useRouter()
const { t } = useLanguage()
if (isLoading) {
return (
<div className="px-4 space-y-2">
{[1, 2, 3].map(i => (
<div key={i} className="h-12 rounded-xl bg-paper/50 animate-pulse" />
))}
</div>
)
}
if (!sessions || sessions.length === 0) {
return (
<div className="px-4 py-6 text-center">
<Sparkles size={20} className="mx-auto text-orange-400/40 mb-2" />
<p className="text-[11px] text-muted-foreground">{t('brainstorm.noSessions')}</p>
<button
onClick={() => router.push('/brainstorm')}
className="mt-2 text-[11px] text-orange-500 hover:text-orange-400 font-medium"
>
{t('brainstorm.startOne')}
</button>
</div>
)
}
return (
<div className="space-y-0.5">
{sessions.slice(0, 10).map(s => (
<div key={s.id} className="relative group/item">
<button
onClick={() => router.replace(`/brainstorm?session=${s.id}`)}
className={`w-full flex items-center gap-3 px-4 py-2.5 rounded-xl transition-all duration-200 text-start hover:bg-memento-blue/5 group ${
(s as any)._owned === false ? 'border-s-2 border-memento-blue/30 dark:border-memento-blue/70' : ''
}`}
>
<div className={`w-7 h-7 rounded-full flex items-center justify-center shrink-0 ${
(s as any)._owned === false
? 'border border-memento-blue/20 dark:border-memento-blue/80 bg-memento-blue/5 dark:bg-memento-blue/20'
: 'border border-orange-200 dark:border-orange-800/40 bg-orange-50 dark:bg-orange-900/20'
}`}>
<Sparkles size={12} className={(s as any)._owned === false ? 'text-memento-blue' : 'text-orange-500'} />
</div>
<div className="min-w-0 flex-1">
<p className="text-[12px] font-medium truncate">
{s.seedIdea}
{(s as any)._owned === false && <span className="text-[9px] ms-1.5 text-memento-blue/70 font-normal">· partagé</span>}
</p>
<p className="text-[10px] text-muted-foreground">
{s.activeIdeas} {t('brainstorm.ideas')} · {new Date(s.createdAt).toLocaleDateString()}
</p>
</div>
</button>
{(s as any)._owned !== false && (
<button
onClick={(e) => {
e.stopPropagation()
deleteBrainstorm.mutate(s.id)
}}
className="absolute end-2 top-1/2 -translate-y-1/2 p-1.5 rounded-lg opacity-0 group-hover/item:opacity-100 hover:bg-rose-500/10 text-muted-foreground hover:text-rose-500 transition-all"
>
<Trash2 size={13} />
</button>
)}
</div>
))}
</div>
)
}
function SidebarCarnetItem({
carnet,
isActive,
@@ -117,7 +196,8 @@ function SidebarCarnetItem({
isExpanded: boolean
toggleExpand: () => void
}) {
const { t } = useLanguage()
const { t, language } = useLanguage()
const isRtl = language === 'fa' || language === 'ar'
const hasChildren = React.Children.count(children) > 0
const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null)
@@ -133,7 +213,7 @@ function SidebarCarnetItem({
<div className={cn('transition-opacity', isDragging && 'opacity-40')}>
<div
className="flex items-center group relative h-10"
style={{ paddingLeft: `${level * 16}px` }}
style={{ paddingInlineStart: `${level * 16}px` }}
onContextMenu={(e) => {
e.preventDefault()
e.stopPropagation()
@@ -141,15 +221,15 @@ function SidebarCarnetItem({
}}
>
{level > 0 && (
<div className="absolute left-[8px] top-[-10px] bottom-1/2 w-px bg-border/40" />
<div className="absolute start-[8px] top-[-10px] bottom-1/2 w-px bg-border/40" />
)}
{level > 0 && (
<div className="absolute left-[8px] top-1/2 w-[8px] h-px bg-border/40" />
<div className="absolute start-[8px] top-1/2 w-[8px] h-px bg-border/40" />
)}
<div
{...dragHandleProps}
className="absolute left-1 top-1/2 -translate-y-1/2 p-1 rounded text-muted-foreground/30 hover:text-muted-foreground cursor-grab active:cursor-grabbing opacity-0 group-hover:opacity-100 transition-opacity z-10"
className="absolute start-1 top-1/2 -translate-y-1/2 p-1 rounded text-muted-foreground/30 hover:text-muted-foreground cursor-grab active:cursor-grabbing opacity-0 group-hover:opacity-100 transition-opacity z-10"
>
<GripVertical size={12} />
</div>
@@ -161,7 +241,7 @@ function SidebarCarnetItem({
className="p-1 hover:bg-foreground/5 rounded-md transition-colors text-muted-foreground"
>
<motion.div animate={{ rotate: isExpanded ? 90 : 0 }} transition={{ duration: 0.2 }}>
<ChevronRight size={14} />
<ChevronRight size={14} className="rtl:scale-x-[-1]" />
</motion.div>
</button>
) : (
@@ -169,7 +249,7 @@ function SidebarCarnetItem({
)}
<motion.div
whileHover={{ x: 2 }}
whileHover={{ x: isRtl ? -2 : 2 }}
onClick={onCarnetClick}
onDoubleClick={(e) => { e.stopPropagation(); onRename() }}
className={cn(
@@ -180,7 +260,7 @@ function SidebarCarnetItem({
{isActive && (
<motion.div
layoutId="active-indicator"
className="absolute -left-1 w-1 h-4 bg-blueprint rounded-full"
className="absolute -start-1 w-1 h-4 bg-blueprint rounded-full"
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
/>
)}
@@ -193,7 +273,7 @@ function SidebarCarnetItem({
{carnet.initial}
</div>
<div className="flex-1 text-left flex items-center gap-2 min-w-0">
<div className="flex-1 text-start flex items-center gap-2 min-w-0">
<span className={cn(
'text-[12px] font-medium transition-colors truncate',
isActive ? 'text-ink' : 'text-muted-ink group-hover/item:text-ink'
@@ -205,7 +285,7 @@ function SidebarCarnetItem({
<div className="flex items-center gap-1 opacity-0 group-hover/item:opacity-100 transition-opacity shrink-0">
{isPinned && (
<span className="text-blueprint" title="Carnet figé">
<span className="text-blueprint" title={t('notebook.pinnedFrozenTooltip')}>
<Pin size={9} className="opacity-70" />
</span>
)}
@@ -258,8 +338,8 @@ function SidebarCarnetItem({
className="w-full flex items-center gap-2.5 px-3 py-2 text-[12px] text-ink hover:bg-foreground/5 transition-colors"
>
{isPinned
? <><PinOff size={13} className="text-blueprint" /><span>Défiger l'état du carnet</span></>
: <><Pin size={13} className="text-blueprint" /><span>Figer l'état du carnet</span></>
? <><PinOff size={13} className="text-blueprint" /><span>{t('sidebar.unfreezePinnedNotebook')}</span></>
: <><Pin size={13} className="text-blueprint" /><span>{t('sidebar.freezePinnedNotebook')}</span></>
}
</button>
<div className="mx-3 my-1 border-t border-border/50" />
@@ -268,14 +348,14 @@ function SidebarCarnetItem({
className="w-full flex items-center gap-2.5 px-3 py-2 text-[12px] text-ink hover:bg-foreground/5 transition-colors"
>
<Plus size={13} className="text-concrete" />
<span>Nouveau sous-carnet</span>
<span>{t('sidebar.newSubNotebook')}</span>
</button>
<button
onClick={() => { onRename(); setContextMenu(null) }}
className="w-full flex items-center gap-2.5 px-3 py-2 text-[12px] text-ink hover:bg-foreground/5 transition-colors"
>
<Pencil size={13} className="text-concrete" />
<span>Renommer</span>
<span>{t('sidebar.renameNotebook')}</span>
</button>
<div className="mx-3 my-1 border-t border-border/50" />
<button
@@ -283,7 +363,7 @@ function SidebarCarnetItem({
className="w-full flex items-center gap-2.5 px-3 py-2 text-[12px] text-rose-500 hover:bg-rose-50 dark:hover:bg-rose-950/30 transition-colors"
>
<Trash2 size={13} />
<span>Supprimer</span>
<span>{t('common.delete')}</span>
</button>
</motion.div>
)}
@@ -298,8 +378,8 @@ function SidebarCarnetItem({
transition={{ duration: 0.3, ease: [0.23, 1, 0.32, 1] }}
className="overflow-hidden"
>
<div className="relative" style={{ marginLeft: `${(level + 1) * 16 + 10}px` }}>
<div className="absolute left-[-6px] top-0 bottom-4 w-px bg-border/30" />
<div className="relative" style={{ marginInlineStart: `${(level + 1) * 16 + 10}px` }}>
<div className="absolute start-[-6px] top-0 bottom-4 w-px bg-border/30" />
<div className="space-y-0.5 py-1">
{children}
@@ -312,7 +392,7 @@ function SidebarCarnetItem({
/>
))}
{isActive && notes.length === 0 && !hasChildren && (
<p className="pl-8 py-2 text-[10px] italic text-muted-foreground/40 font-light">
<p className="ps-8 py-2 text-[10px] italic text-muted-foreground/40 font-light">
{t('common.noResults') || 'No notes found'}
</p>
)}
@@ -329,7 +409,8 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
const pathname = usePathname()
const searchParams = useSearchParams()
const router = useRouter()
const { t } = useLanguage()
const { t, language } = useLanguage()
const isRtl = language === 'fa' || language === 'ar'
const { notebooks, trashNotebook, updateNotebookOrderOptimistic } = useNotebooks()
const { refreshKey } = useNoteRefresh()
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
@@ -733,7 +814,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
className="text-destructive focus:text-destructive"
onClick={() => signOut({ callbackUrl: '/login' })}
>
<LogOut className="h-4 w-4 mr-2" />
<LogOut className="h-4 w-4 me-2" />
{t('sidebar.signOut') || 'Se déconnecter'}
</DropdownMenuItem>
</DropdownMenuContent>
@@ -770,6 +851,13 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
>
<Bot size={14} />
</button>
<button
onClick={() => { setActiveView('brainstorms'); router.push('/brainstorm') }}
className={cn('p-1.5 rounded-full transition-all', activeView === 'brainstorms' ? 'bg-orange-500 text-white shadow-sm' : 'text-muted-ink hover:text-ink')}
title={t('brainstorm.sessions')}
>
<Sparkles size={14} />
</button>
</div>
</div>
</div>
@@ -781,9 +869,9 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
{activeView === 'notebooks' ? (
<motion.div
key="notebooks"
initial={{ opacity: 0, x: -10 }}
initial={{ opacity: 0, x: isRtl ? 10 : -10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 10 }}
exit={{ opacity: 0, x: isRtl ? -10 : 10 }}
transition={{ duration: 0.2 }}
>
{/* Section header with sort button */}
@@ -795,7 +883,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
<button
onClick={() => { setCreateParentId(null); setIsCreateDialogOpen(true) }}
className="p-1 text-muted-foreground hover:text-foreground hover:bg-white/40 transition-all rounded"
title={t('notebook.create') || 'Nouveau carnet'}
title={t('notebook.create')}
>
<Plus size={12} />
</button>
@@ -813,14 +901,14 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
initial={{ opacity: 0, scale: 0.9, y: -4 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: -4 }}
className="absolute right-0 top-full mt-1 bg-card border border-border rounded-xl shadow-lg z-50 py-1 min-w-[140px]"
className="absolute end-0 top-full mt-1 bg-card border border-border rounded-xl shadow-lg z-50 py-1 min-w-[140px]"
>
{(['newest', 'oldest', 'alpha'] as SortOrder[]).map(order => (
<button
key={order}
onClick={() => { setSortOrder(order); setShowSortMenu(false) }}
className={cn(
'w-full text-left px-4 py-2 text-[12px] transition-colors',
'w-full text-start px-4 py-2 text-[12px] transition-colors',
sortOrder === order
? 'font-bold text-foreground'
: 'text-muted-foreground hover:text-foreground hover:bg-muted/40'
@@ -872,9 +960,9 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
) : activeView === 'reminders' ? (
<motion.div
key="reminders"
initial={{ opacity: 0, x: -10 }}
initial={{ opacity: 0, x: isRtl ? 10 : -10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 10 }}
exit={{ opacity: 0, x: isRtl ? -10 : 10 }}
transition={{ duration: 0.2 }}
>
<p className="text-[10px] font-bold text-muted-ink tracking-[0.2em] uppercase mb-4 px-4">
@@ -885,12 +973,12 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
<p className="text-[11px] text-concrete italic">{t('sidebar.noReminders') || 'No active reminders.'}</p>
</div>
</motion.div>
) : (
) : activeView === 'agents' ? (
<motion.div
key="agents"
initial={{ opacity: 0, x: 10 }}
initial={{ opacity: 0, x: isRtl ? -10 : 10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -10 }}
exit={{ opacity: 0, x: isRtl ? 10 : -10 }}
transition={{ duration: 0.2 }}
>
<p className="text-[10px] font-bold text-muted-foreground tracking-widest uppercase mb-4 px-4">
@@ -900,6 +988,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
{[
{ id: 'agents', href: '/agents', label: t('agents.myAgents'), icon: Bot },
{ id: 'lab', href: '/lab', label: t('nav.lab'), icon: FlaskConical },
{ id: 'brainstorm', href: '/brainstorm', label: t('brainstorm.sessions'), icon: Sparkles },
].map(item => {
const isActive = pathname.startsWith(item.href)
return (
@@ -925,6 +1014,28 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
})}
</div>
</motion.div>
) : (
<motion.div
key="brainstorms"
initial={{ opacity: 0, x: isRtl ? -10 : 10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: isRtl ? 10 : -10 }}
transition={{ duration: 0.2 }}
>
<div className="flex items-center justify-between px-4 mb-3">
<p className="text-[10px] font-bold text-muted-foreground tracking-widest uppercase">
{t('brainstorm.sessions')}
</p>
<button
onClick={() => router.push('/brainstorm')}
className="p-1 text-muted-foreground hover:text-orange-500 transition-colors rounded"
title={t('brainstorm.newBrainstorm')}
>
<Plus size={12} />
</button>
</div>
<SidebarBrainstorms />
</motion.div>
)}
</AnimatePresence>
</div>
@@ -965,7 +1076,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
<Trash2 size={14} className={pathname === '/trash' ? 'text-rose-500' : 'text-muted-ink group-hover:text-rose-500'} />
<span>{t('sidebar.trash')}</span>
{trashCount > 0 && (
<span className="ml-auto w-1.5 h-1.5 rounded-full bg-rose-400" />
<span className="ms-auto w-1.5 h-1.5 rounded-full bg-rose-400" />
)}
</Link>