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

@@ -6,6 +6,7 @@ import { createShareRequest, removeCollaborator, getNoteCollaborators } from '@/
import { toast } from 'sonner'
import { X, UserPlus, Users, Mail, Trash2, Loader2, Share2, Check } from 'lucide-react'
import { cn } from '@/lib/utils'
import { useLanguage } from '@/lib/i18n'
interface Collaborator {
id: string
@@ -21,6 +22,7 @@ interface NoteShareDialogProps {
}
export function NoteShareDialog({ noteId, noteTitle, onClose }: NoteShareDialogProps) {
const { t } = useLanguage()
const [email, setEmail] = useState('')
const [permission, setPermission] = useState<'view' | 'edit'>('view')
const [collaborators, setCollaborators] = useState<Collaborator[]>([])
@@ -60,13 +62,13 @@ export function NoteShareDialog({ noteId, noteTitle, onClose }: NoteShareDialogP
await createShareRequest(noteId, trimmed, permission)
setSent(true)
setEmail('')
toast.success(`Invitation envoyée à ${trimmed}`)
toast.success(t('collaboration.toastInviteSentTo', { email: trimmed }))
setTimeout(() => setSent(false), 2000)
loadCollaborators()
} catch (err: any) {
const msg = err?.message || 'Erreur lors du partage'
if (msg.includes('not found')) toast.error('Aucun compte trouvé avec cet email.')
else if (msg.includes('already shared')) toast.error('Cette note est déjà partagée avec cet utilisateur.')
const msg = err?.message || t('collaboration.toastSharingError')
if (msg.includes('not found')) toast.error(t('collaboration.toastEmailNotFound'))
else if (msg.includes('already shared')) toast.error(t('collaboration.toastAlreadySharedUser'))
else toast.error(msg)
} finally {
setSending(false)
@@ -78,9 +80,11 @@ export function NoteShareDialog({ noteId, noteTitle, onClose }: NoteShareDialogP
try {
await removeCollaborator(noteId, collaboratorId)
setCollaborators(prev => prev.filter(c => c.id !== collaboratorId))
toast.success(`Accès retiré à ${collaboratorEmail || "l'utilisateur"}`)
toast.success(t('collaboration.toastAccessRemoved', {
target: collaboratorEmail || t('collaboration.toastUserFallback'),
}))
} catch {
toast.error("Impossible de retirer l'accès.")
toast.error(t('collaboration.toastRemoveAccessFailed'))
} finally {
setRemovingId(null)
}
@@ -100,8 +104,8 @@ export function NoteShareDialog({ noteId, noteTitle, onClose }: NoteShareDialogP
{/* Header */}
<div className="px-6 pt-6 pb-4 border-b border-black/10 dark:border-white/10 flex items-start justify-between">
<div className="flex items-center gap-2">
<Share2 size={15} className="text-[#75B2D6]" />
<h2 className="text-sm font-bold text-foreground tracking-tight">Partager</h2>
<Share2 size={15} className="text-[#A47148]" />
<h2 className="text-sm font-bold text-foreground tracking-tight">{t('collaboration.shareCompactTitle')}</h2>
</div>
<button
onClick={onClose}
@@ -114,7 +118,7 @@ export function NoteShareDialog({ noteId, noteTitle, onClose }: NoteShareDialogP
{/* Invite form */}
<form onSubmit={handleInvite} className="px-6 py-5 space-y-3">
<label className="text-[9px] uppercase tracking-[0.25em] font-bold text-foreground/40">
Inviter par email
{t('collaboration.inviteByEmailLabel')}
</label>
<div className="flex gap-2">
<div className="relative flex-1">
@@ -126,7 +130,7 @@ export function NoteShareDialog({ noteId, noteTitle, onClose }: NoteShareDialogP
onChange={e => setEmail(e.target.value)}
required
autoFocus
className="w-full pl-9 pr-3 py-2.5 text-[13px] rounded-xl border border-black/15 dark:border-white/15 bg-transparent outline-none focus:ring-2 ring-[#75B2D6]/30 focus:border-[#75B2D6] transition-all placeholder:text-foreground/30"
className="w-full pl-9 pr-3 py-2.5 text-[13px] rounded-xl border border-black/15 dark:border-white/15 bg-transparent outline-none focus:ring-2 ring-[#A47148]/30 focus:border-[#A47148] transition-all placeholder:text-foreground/30"
/>
</div>
{/* Permission toggle */}
@@ -139,11 +143,11 @@ export function NoteShareDialog({ noteId, noteTitle, onClose }: NoteShareDialogP
className={cn(
'px-3 py-2 text-[10px] font-bold uppercase tracking-wide transition-colors',
permission === p
? 'bg-[#75B2D6] text-white'
? 'bg-[#A47148] text-white'
: 'text-foreground/50 hover:bg-black/5 dark:hover:bg-white/5'
)}
>
{p === 'view' ? 'Lire' : 'Éditer'}
{p === 'view' ? t('collaboration.accessReadCompact') : t('collaboration.accessEditCompact')}
</button>
))}
</div>
@@ -158,14 +162,14 @@ export function NoteShareDialog({ noteId, noteTitle, onClose }: NoteShareDialogP
? 'bg-black/5 dark:bg-white/5 text-foreground/30 cursor-not-allowed'
: sent
? 'bg-emerald-500 text-white'
: 'bg-[#75B2D6] text-white hover:opacity-90 shadow-sm shadow-[#75B2D6]/30'
: 'bg-[#A47148] text-white hover:opacity-90 shadow-sm shadow-[#A47148]/30'
)}
>
{sending
? <Loader2 size={13} className="animate-spin" />
: sent
? <><Check size={13} /> Invitation envoyée</>
: <><UserPlus size={13} /> Envoyer l&apos;invitation</>
? <><Check size={13} /> {t('collaboration.invitationSentBadge')}</>
: <><UserPlus size={13} /> {t('collaboration.sendInvitation')}</>
}
</button>
</form>
@@ -175,7 +179,7 @@ export function NoteShareDialog({ noteId, noteTitle, onClose }: NoteShareDialogP
<div className="flex items-center gap-2">
<div className="h-px flex-1 bg-black/10 dark:bg-white/10" />
<span className="text-[9px] uppercase tracking-[0.25em] font-bold text-foreground/30 flex items-center gap-1.5">
<Users size={10} /> Accès partagé
<Users size={10} /> {t('collaboration.sharedAccessLabel')}
</span>
<div className="h-px flex-1 bg-black/10 dark:bg-white/10" />
</div>
@@ -186,7 +190,7 @@ export function NoteShareDialog({ noteId, noteTitle, onClose }: NoteShareDialogP
</div>
) : collaborators.length === 0 ? (
<p className="text-center text-[11px] text-foreground/30 py-4">
Aucun collaborateur pour l&apos;instant.
{t('collaboration.noCollaboratorsEmpty')}
</p>
) : (
<ul className="space-y-2">
@@ -199,14 +203,14 @@ export function NoteShareDialog({ noteId, noteTitle, onClose }: NoteShareDialogP
}
</div>
<div className="flex-1 min-w-0">
<p className="text-[12px] font-semibold text-foreground truncate">{c.name || 'Utilisateur'}</p>
<p className="text-[12px] font-semibold text-foreground truncate">{c.name || t('collaboration.userFallback')}</p>
<p className="text-[10px] text-foreground/40 truncate">{c.email}</p>
</div>
<button
onClick={() => handleRemove(c.id, c.email)}
disabled={removingId === c.id}
className="p-1.5 rounded-lg text-foreground/30 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-950/30 transition-colors disabled:opacity-50"
title="Retirer l'accès"
title={t('collaboration.removeAccessTitle')}
>
{removingId === c.id ? <Loader2 size={13} className="animate-spin" /> : <Trash2 size={13} />}
</button>