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
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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user