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

@@ -4,13 +4,14 @@ import { useState, useEffect, useCallback } from 'react'
import { useRouter } from 'next/navigation'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Bell, Check, X, Clock, AlertCircle, CheckCircle2, Circle, Share2, Bot, Trash2, Download, Pencil, Presentation } from 'lucide-react'
import { Bell, Check, X, Clock, AlertCircle, CheckCircle2, Circle, Share2, Bot, Trash2, Download, Pencil, Presentation, Wind } from 'lucide-react'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
import { getPendingShareRequests, respondToShareRequest, getNotesWithReminders, toggleReminderDone } from '@/app/actions/notes'
import { getPendingBrainstormShares, respondToBrainstormShare } from '@/app/actions/brainstorm'
import { getUnreadNotifications, markNotificationRead, markAllNotificationsRead, type AppNotification } from '@/app/actions/notifications'
import { toast } from 'sonner'
import { useRefresh } from '@/lib/use-refresh'
@@ -60,23 +61,29 @@ export function NotificationPanel() {
const { t } = useLanguage()
const router = useRouter()
const [requests, setRequests] = useState<ShareRequest[]>([])
const [brainstormShares, setBrainstormShares] = useState<any[]>([])
const [reminders, setReminders] = useState<ReminderNote[]>([])
const [appNotifications, setAppNotifications] = useState<AppNotification[]>([])
const [isLoading, setIsLoading] = useState(false)
const [open, setOpen] = useState(false)
const loadData = useCallback(async () => {
setIsLoading(true)
try {
const [shareData, reminderData, notifData] = await Promise.all([
const [shareData, brainstormData, reminderData, notifData] = await Promise.all([
getPendingShareRequests(),
getPendingBrainstormShares(),
getNotesWithReminders(),
getUnreadNotifications(),
])
setRequests(shareData as any)
setBrainstormShares(brainstormData as any || [])
setReminders((reminderData as any) || [])
setAppNotifications(notifData || [])
} catch (error: any) {
console.error('Failed to load notifications:', error)
} finally {
setIsLoading(false)
}
}, [])
@@ -96,7 +103,7 @@ export function NotificationPanel() {
const overdueReminders = activeReminders.filter(r => new Date(r.reminder!) < now)
const upcomingReminders = activeReminders.filter(r => new Date(r.reminder!) >= now)
const pendingCount = requests.length + overdueReminders.length + appNotifications.length
const pendingCount = requests.length + brainstormShares.length + overdueReminders.length + appNotifications.length
const handleAccept = async (shareId: string) => {
try {
@@ -144,7 +151,29 @@ export function NotificationPanel() {
setAppNotifications([])
}
const hasContent = requests.length > 0 || activeReminders.length > 0 || appNotifications.length > 0
const handleAcceptBrainstorm = async (shareId: string) => {
try {
await respondToBrainstormShare(shareId, 'accept')
setBrainstormShares(prev => prev.filter(s => s.id !== shareId))
toast.success(t('notification.accepted') || 'Accepted')
setOpen(false)
} catch (error: any) {
toast.error(error.message || t('general.error'))
}
}
const handleDeclineBrainstorm = async (shareId: string) => {
try {
await respondToBrainstormShare(shareId, 'decline')
setBrainstormShares(prev => prev.filter(s => s.id !== shareId))
toast.info(t('notification.declined') || 'Declined')
if (brainstormShares.length <= 1) setOpen(false)
} catch (error: any) {
toast.error(error.message || t('general.error'))
}
}
const hasContent = requests.length > 0 || brainstormShares.length > 0 || activeReminders.length > 0 || appNotifications.length > 0
// ── icon bg/color per notification type ──────────────────────────────────
const notifIconStyle = (type: string) => {
@@ -152,13 +181,17 @@ export function NotificationPanel() {
if (type === 'agent_slides_ready') return { bg: `${C.gold}20`, color: C.gold }
if (type === 'agent_canvas_ready') return { bg: `${C.gold}20`, color: C.gold }
if (type === 'agent_failure') return { bg: '#EF444420', color: '#EF4444' }
if (type === 'brainstorm_invite') return { bg: '#10b98120', color: '#10b981' }
if (type === 'brainstorm_joined') return { bg: '#60a5fa20', color: '#60a5fa' }
return { bg: `${C.green}20`, color: C.green }
}
const notifLabelColor = (type: string) => {
if (type.startsWith('agent')) {
if (type === 'agent_failure') return '#EF4444'
return C.gold
if (type === 'agent_failure') return '#EF4444'
if (type === 'brainstorm_invite') return '#10b981'
if (type === 'brainstorm_joined') return '#60a5fa'
return C.gold
}
return C.green
}
@@ -249,6 +282,8 @@ export function NotificationPanel() {
>
{isSlides ? <Presentation className="w-3.5 h-3.5" />
: isCanvas ? <Pencil className="w-3.5 h-3.5" />
: notif.type === 'brainstorm_invite' ? <Wind className="w-3.5 h-3.5" />
: notif.type === 'brainstorm_joined' ? <Wind className="w-3.5 h-3.5" />
: notif.type.startsWith('agent') ? <Bot className="w-3.5 h-3.5" />
: <AlertCircle className="w-3.5 h-3.5" />}
</div>
@@ -262,7 +297,9 @@ export function NotificationPanel() {
{notif.type === 'agent_canvas_ready' && (t('notification.canvasReady') || 'Diagramme prêt')}
{notif.type === 'agent_success' && (t('notification.agentSuccess') || 'Agent terminé')}
{notif.type === 'agent_failure' && (t('notification.agentFailed') || 'Agent échoué')}
{notif.type === 'system' && 'Système'}
{notif.type === 'brainstorm_invite' && (t('notification.brainstormInvite') || 'Brainstorm')}
{notif.type === 'brainstorm_joined' && (t('notification.brainstormJoined') || 'Brainstorm')}
{notif.type === 'system' && t('notification.systemNotification')}
</span>
<p className="text-[13px] font-semibold truncate mt-0.5">{notif.title}</p>
{notif.message && (
@@ -302,7 +339,7 @@ export function NotificationPanel() {
a.download = parsed.filename || `${data.canvas.name || 'presentation'}.pptx`
document.body.appendChild(a); a.click()
document.body.removeChild(a); URL.revokeObjectURL(url)
} catch { toast.error('Échec du téléchargement') }
} catch { toast.error(t('notification.downloadFailed')) }
}}
className="flex items-center gap-1.5 px-3 py-1.5 text-[10px] font-bold rounded-lg text-white uppercase tracking-wide transition-all hover:opacity-90 active:scale-95 shadow-sm"
style={{ background: C.blue }}
@@ -362,6 +399,51 @@ export function NotificationPanel() {
</div>
))}
{/* ── Brainstorm share invites ── */}
{brainstormShares.map((share) => (
<div key={share.id} className="p-4 hover:bg-black/[0.02] transition-colors space-y-3">
<div className="flex items-start gap-3">
<div
className="h-8 w-8 rounded-full flex items-center justify-center text-white font-bold text-[11px] shrink-0 shadow-sm"
style={{ background: `linear-gradient(135deg, #fb923c, #f97316)` }}
>
{(share.sharer?.name || share.sharer?.email || '?')[0].toUpperCase()}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-1.5 mb-0.5">
<Wind className="w-3 h-3" style={{ color: '#fb923c' }} />
<span className="text-[9px] font-bold uppercase tracking-[0.2em]" style={{ color: '#fb923c' }}>
Brainstorm
</span>
</div>
<p className="text-[13px] font-semibold truncate">
{share.sharer?.name || share.sharer?.email}
</p>
<p className="text-[11px] text-foreground/50 truncate">
{t('notification.brainstormShared') || 'invited you to a brainstorm'} « {share.session?.seedIdea?.length > 35 ? share.session.seedIdea.substring(0, 35) + '…' : share.session?.seedIdea} »
</p>
</div>
</div>
<div className="flex gap-2 ml-11">
<button
onClick={() => handleDeclineBrainstorm(share.id)}
className="flex-1 h-7 px-3 text-[11px] font-semibold rounded-lg border border-black/15 text-foreground/60 hover:bg-black/5 transition-all active:scale-95 flex items-center justify-center gap-1"
>
<X className="h-3 w-3" />
{t('notification.decline') || 'Decline'}
</button>
<button
onClick={() => handleAcceptBrainstorm(share.id)}
className="flex-1 h-7 px-3 text-[11px] font-bold rounded-lg text-white transition-all active:scale-95 flex items-center justify-center gap-1 shadow-sm hover:opacity-90"
style={{ background: '#fb923c' }}
>
<Check className="h-3 w-3" />
{t('notification.accept') || 'Accept'}
</button>
</div>
</div>
))}
{/* ── Share requests ── */}
{requests.map((request) => (
<div key={request.id} className="p-4 hover:bg-black/[0.02] transition-colors space-y-3">