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

@@ -11,6 +11,7 @@ import {
type OrganizationPlan,
} from '@/app/actions/organize-notebook'
import { toast } from 'sonner'
import { useLanguage } from '@/lib/i18n'
interface OrganizeNotebookDialogProps {
open: boolean
@@ -29,6 +30,8 @@ export function OrganizeNotebookDialog({
notebookName,
onDone,
}: OrganizeNotebookDialogProps) {
const { t, language } = useLanguage()
const organizePanelSlide = language === 'fa' || language === 'ar' ? -60 : 60
const [step, setStep] = useState<Step>('idle')
const [plan, setPlan] = useState<OrganizationPlan | null>(null)
const [editableGroups, setEditableGroups] = useState<OrganizationGroup[]>([])
@@ -44,7 +47,7 @@ export function OrganizeNotebookDialog({
const res = await analyzeNotebookForOrganization(notebookId)
if (!res.success || !res.plan) {
setError(res.error ?? 'Erreur inconnue')
setError(res.error ?? t('organizeNotebook.unknownError'))
setStep('idle')
return
}
@@ -94,14 +97,14 @@ export function OrganizeNotebookDialog({
const res = await executeNotebookOrganization(finalPlan)
if (!res.success) {
setError(res.error ?? 'Erreur inconnue')
setError(res.error ?? t('organizeNotebook.unknownError'))
setStep('preview')
return
}
setResult({ created: res.created, moved: res.moved })
setStep('done')
toast.success(`Carnet organisé — ${res.created} sous-carnet(s) créé(s), ${res.moved} note(s) déplacée(s)`)
toast.success(t('organizeNotebook.toastSuccess', { created: res.created, moved: res.moved }))
onDone?.()
}, [plan, editableGroups, onDone])
@@ -138,11 +141,11 @@ export function OrganizeNotebookDialog({
{/* Panel */}
<motion.div
initial={{ opacity: 0, x: 60 }}
initial={{ opacity: 0, x: organizePanelSlide }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 60 }}
exit={{ opacity: 0, x: organizePanelSlide }}
transition={{ type: 'spring', stiffness: 280, damping: 30 }}
className="fixed right-0 top-0 bottom-0 z-50 w-[460px] bg-card border-l border-border shadow-2xl flex flex-col"
className="fixed end-0 top-0 bottom-0 z-50 w-[460px] bg-card border-s border-border shadow-2xl flex flex-col"
onClick={e => e.stopPropagation()}
>
{/* Header */}
@@ -152,7 +155,7 @@ export function OrganizeNotebookDialog({
<Sparkles size={16} className="text-blueprint" />
</div>
<div>
<h2 className="text-[13px] font-semibold text-ink">Organiser le carnet</h2>
<h2 className="text-[13px] font-semibold text-ink">{t('organizeNotebook.title')}</h2>
<p className="text-[11px] text-muted-ink truncate max-w-[240px]">{notebookName}</p>
</div>
</div>
@@ -186,14 +189,10 @@ export function OrganizeNotebookDialog({
)}
<div className="space-y-3">
<p className="text-[13px] text-ink leading-relaxed">
L'IA va analyser les notes de ce carnet et vous proposer un plan de réorganisation en sous-carnets thématiques.
{t('organizeNotebook.intro')}
</p>
<ul className="space-y-2">
{[
'Regroupement par sujet ou thème',
'Création de sous-carnets manquants',
'Aperçu complet avant modification',
].map(item => (
{[t('organizeNotebook.bulletThemes'), t('organizeNotebook.bulletSubfolders'), t('organizeNotebook.bulletPreview')].map(item => (
<li key={item} className="flex items-center gap-2 text-[12px] text-muted-ink">
<div className="w-1.5 h-1.5 rounded-full bg-blueprint shrink-0" />
{item}
@@ -224,8 +223,8 @@ export function OrganizeNotebookDialog({
/>
</div>
<div className="text-center space-y-1.5">
<p className="text-[14px] font-medium text-ink">Analyse en cours…</p>
<p className="text-[12px] text-muted-ink">L'IA lit vos notes et identifie les thèmes</p>
<p className="text-[14px] font-medium text-ink">{t('organizeNotebook.analyzingTitle')}</p>
<p className="text-[12px] text-muted-ink">{t('organizeNotebook.analyzingSubtitle')}</p>
</div>
<div className="flex gap-1.5">
{[0, 1, 2].map(i => (
@@ -253,7 +252,11 @@ export function OrganizeNotebookDialog({
<div className="flex items-center gap-3 p-3 rounded-xl bg-blueprint/5 border border-blueprint/20">
<Sparkles size={12} className="text-blueprint shrink-0" />
<p className="text-[11px] text-blueprint font-medium">
{editableGroups.length} groupe(s) · {totalNotes} note(s) · {newSubNbs} nouveau(x) sous-carnet(s)
{t('organizeNotebook.previewSummary', {
groups: editableGroups.length,
notes: totalNotes,
newSubs: newSubNbs,
})}
</p>
</div>
@@ -290,7 +293,7 @@ export function OrganizeNotebookDialog({
/>
{group.isNew && (
<span className="px-1.5 py-0.5 rounded text-[9px] font-bold uppercase tracking-wider bg-blueprint/10 text-blueprint shrink-0">
Nouveau
{t('organizeNotebook.badgeNew')}
</span>
)}
<div className="flex items-center gap-1 shrink-0">
@@ -329,7 +332,7 @@ export function OrganizeNotebookDialog({
<Check size={10} className="text-blueprint" />
</div>
<span className="text-[11px] text-muted-ink truncate group-hover:text-ink transition-colors">
{note.title || 'Note sans titre'}
{note.title || t('organizeNotebook.untitledNote')}
</span>
</div>
))}
@@ -341,7 +344,7 @@ export function OrganizeNotebookDialog({
{/* Collapsed count */}
{!expandedGroups.has(idx) && (
<div className="px-4 pb-2.5 text-[11px] text-muted-ink/60">
{group.notes.length} note(s)
{t('organizeNotebook.notesInGroup', { count: group.notes.length })}
</div>
)}
</motion.div>
@@ -360,8 +363,8 @@ export function OrganizeNotebookDialog({
>
<Loader2 size={32} className="text-blueprint animate-spin" />
<div className="text-center space-y-1">
<p className="text-[14px] font-medium text-ink">Organisation en cours</p>
<p className="text-[12px] text-muted-ink">Création des sous-carnets et déplacement des notes</p>
<p className="text-[14px] font-medium text-ink">{t('organizeNotebook.executingTitle')}</p>
<p className="text-[12px] text-muted-ink">{t('organizeNotebook.executingSubtitle')}</p>
</div>
</motion.div>
)}
@@ -378,10 +381,10 @@ export function OrganizeNotebookDialog({
<CheckCircle2 size={32} className="text-emerald-500" />
</div>
<div className="text-center space-y-1.5">
<p className="text-[15px] font-semibold text-ink">Carnet organisé !</p>
<p className="text-[15px] font-semibold text-ink">{t('organizeNotebook.doneTitle')}</p>
{result && (
<p className="text-[12px] text-muted-ink">
{result.created} sous-carnet(s) créé(s) · {result.moved} note(s) déplacée(s)
{t('organizeNotebook.doneStats', { created: result.created, moved: result.moved })}
</p>
)}
</div>
@@ -399,7 +402,7 @@ export function OrganizeNotebookDialog({
className="w-full flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl bg-ink text-paper text-[13px] font-semibold hover:opacity-85 transition-opacity"
>
<Sparkles size={14} />
Analyser avec l'IA
{t('organizeNotebook.analyzeButton')}
</button>
)}
@@ -409,7 +412,7 @@ export function OrganizeNotebookDialog({
onClick={() => { setStep('idle'); setError(null) }}
className="flex-1 px-4 py-2.5 rounded-xl border border-border text-[13px] font-medium text-muted-ink hover:text-ink transition-colors"
>
Recommencer
{t('organizeNotebook.restart')}
</button>
<button
onClick={handleExecute}
@@ -422,7 +425,7 @@ export function OrganizeNotebookDialog({
)}
>
<Check size={14} />
Valider
{t('organizeNotebook.confirm')}
</button>
</div>
)}
@@ -432,7 +435,7 @@ export function OrganizeNotebookDialog({
onClick={handleClose}
className="w-full px-4 py-2.5 rounded-xl border border-border text-[13px] font-medium text-muted-ink hover:text-ink transition-colors"
>
Fermer
{t('organizeNotebook.closeButton')}
</button>
)}
</div>